/*
 * Copyright © 2013-2018 Metreeca srl. All rights reserved.
 *
 * This file is part of Metreeca.
 *
 * Metreeca is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Metreeca is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Metreeca. If not, see <http://www.gnu.org/licenses/>.
 */

package com.metreeca.data;


import com.metreeca.link.*;
import com.metreeca.tray.Tray;
import com.metreeca.tray.sys.Setup;
import com.metreeca.tray.sys.Trace;

import java.io.*;
import java.net.MalformedURLException;
import java.util.*;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import static com.metreeca.tray.sys.Setup.SetupProperty;

import static java.lang.String.format;
import static java.util.Arrays.asList;


public class Data implements Filter {

	private Tray tray;


	@Override public void init(final FilterConfig config) throws ServletException {
		tray=(Tray)config.getServletContext().getAttribute(Loader.TrayAttribute);
	}

	@Override public void destroy() {
		tray=null;
	}


	@Override public void doFilter(
			final ServletRequest request, final ServletResponse response, final FilterChain chain
	) throws ServletException, IOException {

		doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);

	}


	////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

	private void doFilter(
			final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain
	) throws IOException, ServletException {

		final String path=path(request);

		tray.get(Server.Tool).serve(tray, request(request), new Response(), (_request, _response) -> {
			try {

				if ( _response.getStatus() == 0 ) { // give other servlets a chance to handle the request

					// include headers generated by the linked data pipeline (e.g. default system headers)

					for (final Map.Entry<String, Collection<String>> header : _response.getHeaders().entrySet()) {
						for (final String value : header.getValue()) { response.addHeader(header.getKey(), value); }
					}

					// request target may be altered by the linked data pipeline to forward to a different resource

					final String base=_request.getBase();
					final String target=_request.getTarget();
					final String resource=target.startsWith(base) ? target.substring(base.length()-1) : path;

					// !!! what if request body was already read? e.g. to test query parameters

					request.getRequestDispatcher(resource).forward(request, response);

				} else if ( _response.getStatus() == Response.OK && _request.isInteractive() ) { // load in interactive deck

					// include headers generated by the linked data pipeline (e.g. default system headers)

					for (final Map.Entry<String, Collection<String>> header : _response.getHeaders().entrySet()) {
						for (final String value : header.getValue()) { response.addHeader(header.getKey(), value); }
					}

					request.getRequestDispatcher("/apps/deck/index.jsp").forward(request, response);

				} else { // generate http response

					response(response, _response);

				}

			} catch ( final IOException e ) {
				throw new UncheckedIOException(e); // !!! review / tunnel
			} catch ( final ServletException e ) {
				throw new RuntimeException(e); // !!! review / tunnel
			}
		});
	}


	private Request request(final HttpServletRequest http) {

		final Request request=new Request()
				.setMethod(http.getMethod())
				.setBase(root(http))
				.setStem(tray.get(Setup.Tool).get(Setup.BaseProperty, "")) // !!! to be removed
				.setTarget(target(http))
				.setQuery(query(http));

		for (final Enumeration<String> names=http.getHeaderNames(); names.hasMoreElements(); ) {

			final String name=names.nextElement();

			for (final Enumeration<String> headers=http.getHeaders(name); headers.hasMoreElements(); ) {

				final String header=headers.nextElement();

				request.addHeader(name, header);
			}
		}

		return request.setBody(() -> {
			try {
				return http.getInputStream();
			} catch ( final IOException e ) {
				throw new UncheckedIOException(e);
			}
		});

	}

	private void response(final HttpServletResponse http, final Response response) {

		http.setStatus(response.getStatus());

		for (final Map.Entry<String, Collection<String>> header : response.getHeaders().entrySet()) {
			for (final String value : header.getValue()) {
				http.addHeader(header.getKey(), value);
			}
		}

		try {
			try (final ServletOutputStream stream=http.getOutputStream()) {

				response.getBody().accept(stream);

			} catch ( final RuntimeException e ) {

				http.sendError(e instanceof IllegalStateException ?
								HttpServletResponse.SC_CONFLICT : HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
						e.getMessage());

			}
		} catch ( final IOException e ) {
			throw new UncheckedIOException(e);
		}
	}


	////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

	private String root(final HttpServletRequest request) {

		final String target=target(request);
		final String path=path(request);

		return target.substring(0, target.length()-path.length()+1);
	}

	private String target(final HttpServletRequest request) {
		return request.getRequestURL().toString();
	}

	private String path(final HttpServletRequest request) {
		return request.getRequestURI().substring(request.getContextPath().length());
	}

	private String query(final HttpServletRequest request) {

		final String query=request.getQueryString();

		return query == null ? "" : query;
	}


	////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

	public static final class Loader implements ServletContextListener {

		private static final String TrayAttribute=Tray.class.getName();


		private final Tray tray=new Tray();


		@Override public void contextInitialized(final ServletContextEvent event) {

			final ServletContext context=event.getServletContext();

			try {

				tray.set(Setup.Tool, tools -> new Setup(asList(Setup::system, Setup::custom, setup -> {

					try { // defaults from WEB-INF directory, if found

						final Properties properties=new Properties();

						Optional.ofNullable(context.getResource("/WEB-INF/"+SetupProperty)).ifPresent(url -> {

							try (final InputStream input=url.openStream()) {
								properties.load(input);
							} catch ( final IOException e ) {
								throw new UncheckedIOException(format("unable to load default setup file [%s]", url), e);
							}

						});

						return properties;

					} catch ( final MalformedURLException unexpected ) {
						throw new UncheckedIOException(unexpected);
					}

				})));

				for (final Toolkit toolkit : ServiceLoader.load(Toolkit.class)) { toolkit.load(tray); }
				for (final Service service : ServiceLoader.load(Service.class)) { service.load(tray); }

				context.setAttribute(TrayAttribute, tray);

			} catch ( final Throwable t ) {

				try (final StringWriter message=new StringWriter()) {

					t.printStackTrace(new PrintWriter(message));

					tray.get(Trace.Tool).error(this, "error during data hub initialization: "+message);
					context.log("error during data hub initialization", t);

					throw t;

				} catch ( final IOException e ) {

					throw new UncheckedIOException(e);

				} finally {

					tray.clear();

				}

			}
		}

		@Override public void contextDestroyed(final ServletContextEvent event) {

			tray.clear();

		}

	}

}
