// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
// * with the License.  You may obtain a copy of the License at                                                              *
// *                                                                                                                         *
// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
// *                                                                                                                         *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
// * specific language governing permissions and limitations under the License.                                              *
// ***************************************************************************************************************************
package org.apache.juneau.dto.swagger.ui;

import static org.apache.juneau.dto.html5.HtmlBuilder.*;

import java.util.*;
import java.util.Map;

import org.apache.juneau.*;
import org.apache.juneau.dto.html5.*;
import org.apache.juneau.dto.swagger.*;
import org.apache.juneau.http.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.transform.*;
import org.apache.juneau.utils.*;

/**
 * Generates a Swagger-UI interface from a Swagger document.
 */
public class SwaggerUI extends PojoSwap<Swagger,Div> {

	//-------------------------------------------------------------------------------------------------------------------
	// Configurable properties
	//-------------------------------------------------------------------------------------------------------------------

	private static final String PREFIX = "SwaggerUI.";

	/**
	 * Configuration property:  Resolve <c>$ref</c> references in schema up to the specified depth.
	 *
	 * <h5 class='section'>Property:</h5>
	 * <ul>
	 * 	<li><b>ID:</b>  {@link org.apache.juneau.dto.swagger.ui.SwaggerUI#SWAGGERUI_resolveRefsMaxDepth SWAGGERUI_resolveRefsMaxDepth}
	 * 	<li><b>Name:</b>  <js>"SwaggerUI.resolveRefsMaxDepth.i"</js>
	 * 	<li><b>Data type:</b>  <jk>int</jk>
	 * 	<li><b>System property:</b>  <c>SwaggerUI.resolveRefsMaxDepth</c>
	 * 	<li><b>Environment variable:</b>  <c>SWAGGERUI_RESOLVEREFSMAXDEPTH</c>
	 * 	<li><b>Default:</b>  <c>1</c>
	 * 	<li><b>Session property:</b>  <jk>true</jk>
	 * </ul>
	 *
	 * <h5 class='section'>Description:</h5>
	 * <p>
	 * Defines the maximum recursive depth to resolve <c>$ref</c> variables in schema infos.
	 * <br>The default <c>1</c> means only resolve the first reference encountered.
	 * <br>A value of <c>0</c> disables reference resolution altogether.
	 *
	 * <h5 class='section'>Example:</h5>
	 * <p class='bcode w800'>
	 * 	<jc>// Resolve schema references up to 5 levels deep.
	 * 	<ja>@Rest</ja>(
	 * 			properties={
	 * 				<ja>@Property</ja>(name=<jsf>SWAGGERUI_resolveRefsMaxDepth</jsf>, value=<js>"5"</js>)
	 * 			}
	 * 	<jk>public class</jk> MyResource {...}
	 * </p>
	 */
	public static final String SWAGGERUI_resolveRefsMaxDepth = PREFIX + "resolveRefsMaxDepth.i";


	static final ClasspathResourceManager RESOURCES = new ClasspathResourceManager(SwaggerUI.class, ClasspathResourceFinderBasic.INSTANCE, Boolean.getBoolean("RestContext.useClasspathResourceCaching.b"));

	private static final Set<String> STANDARD_METHODS = new ASet<String>().appendAll("get", "put", "post", "delete", "options");

	/**
	 * This UI applies to HTML requests only.
	 */
	@Override
	public MediaType[] forMediaTypes() {
		return new MediaType[] {MediaType.HTML};
	}

	private static final class Session {
		final int resolveRefsMaxDepth;
		final Swagger swagger;

		Session(BeanSession bs, Swagger swagger) {
			this.swagger = swagger.copy();
			this.resolveRefsMaxDepth = bs.getProperty(SWAGGERUI_resolveRefsMaxDepth, Integer.class, 1);
		}
	}

	@Override
	public Div swap(BeanSession beanSession, Swagger swagger) throws Exception {

		Session s = new Session(beanSession, swagger);

		String css = RESOURCES.getString("files/htdocs/styles/SwaggerUI.css");
		if (css == null)
			css = RESOURCES.getString("SwaggerUI.css");

		Div outer = div(
			style(css),
			script("text/javascript", new String[]{RESOURCES.getString("SwaggerUI.js")}),
			header(s)
		)._class("swagger-ui");

		// Operations without tags are rendered first.
		outer.child(div()._class("tag-block tag-block-open").children(tagBlockContents(s, null)));

		if (s.swagger.hasTags()) {
			for (Tag t : s.swagger.getTags()) {
				Div tagBlock = div()._class("tag-block tag-block-open").children(
					tagBlockSummary(t),
					tagBlockContents(s, t)
				);
				outer.child(tagBlock);
			}
		}

		if (s.swagger.hasDefinitions()) {
			Div modelBlock = div()._class("tag-block").children(
				modelsBlockSummary(),
				modelsBlockContents(s)
			);
			outer.child(modelBlock);
		}

		return outer;
	}

	// Creates the informational summary before the ops.
	private Table header(Session s) {
		Table table = table()._class("header");

		Info info = s.swagger.getInfo();
		if (info != null) {

			if (info.hasDescription())
				table.child(tr(th("Description:"),td(toBRL(info.getDescription()))));

			if (info.hasVersion())
				table.child(tr(th("Version:"),td(info.getVersion())));

			Contact c = info.getContact();
			if (c != null) {
				Table t2 = table();

				if (c.hasName())
					t2.child(tr(th("Name:"),td(c.getName())));
				if (c.hasUrl())
					t2.child(tr(th("URL:"),td(a(c.getUrl(), c.getUrl()))));
				if (c.hasEmail())
					t2.child(tr(th("Email:"),td(a("mailto:"+ c.getEmail(), c.getEmail()))));

				table.child(tr(th("Contact:"),td(t2)));
			}

			License l = info.getLicense();
			if (l != null) {
				Object child = l.hasUrl() ? a(l.getUrl(), l.hasName() ? l.getName() : l.getUrl()) : l.getName();
				table.child(tr(th("License:"),td(child)));
			}

			ExternalDocumentation ed = s.swagger.getExternalDocs();
			if (ed != null) {
				Object child = ed.hasUrl() ? a(ed.getUrl(), ed.hasDescription() ? ed.getDescription() : ed.getUrl()) : ed.getDescription();
				table.child(tr(th("Docs:"),td(child)));
			}

			if (info.hasTermsOfService()) {
				String tos = info.getTermsOfService();
				Object child = StringUtils.isUri(tos) ? a(tos, tos) : tos;
				table.child(tr(th("Terms of Service:"),td(child)));
			}
		}

		return table;
	}

	// Creates the "pet  Everything about your Pets  ext-link" header.
	private HtmlElement tagBlockSummary(Tag t) {
		ExternalDocumentation ed = t.getExternalDocs();

		return div()._class("tag-block-summary").children(
			span(t.getName())._class("name"),
			span(toBRL(t.getDescription()))._class("description"),
			ed == null ? null : span(a(ed.getUrl(), ed.hasDescription() ? ed.getDescription() : ed.getUrl()))._class("extdocs")
		).onclick("toggleTagBlock(this)");
	}

	// Creates the contents under the "pet  Everything about your Pets  ext-link" header.
	private Div tagBlockContents(Session s, Tag t) {
		Div tagBlockContents = div()._class("tag-block-contents");

		for (Map.Entry<String,OperationMap> e : s.swagger.getPaths().entrySet()) {
			String path = e.getKey();
			for (Map.Entry<String,Operation> e2 : e.getValue().entrySet()) {
				String opName = e2.getKey();
				Operation op = e2.getValue();
				if ((t == null && op.hasNoTags()) || (t != null && op.hasTag(t.getName())))
					tagBlockContents.child(opBlock(s, path, opName, op));
			}
		}

		return tagBlockContents;
	}

	private Div opBlock(Session s, String path, String opName, Operation op) {

		String opClass = op.isDeprecated() ? "deprecated" : opName.toLowerCase();
		if (! op.isDeprecated() && ! STANDARD_METHODS.contains(opClass))
			opClass = "other";

		return div()._class("op-block op-block-closed " + opClass).children(
			opBlockSummary(path, opName, op),
			div(tableContainer(s, op))._class("op-block-contents")
		);
	}

	private HtmlElement opBlockSummary(String path, String opName, Operation op) {
		return div()._class("op-block-summary").children(
			span(opName.toUpperCase())._class("method-button"),
			span(path)._class("path"),
			op.hasSummary() ? span(op.getSummary())._class("summary") : null
		).onclick("toggleOpBlock(this)");
	}

	private Div tableContainer(Session s, Operation op) {
		Div tableContainer = div()._class("table-container");

		if (op.hasDescription())
			tableContainer.child(div(toBRL(op.getDescription()))._class("op-block-description"));

		if (op.hasParameters()) {
			tableContainer.child(div(h4("Parameters")._class("title"))._class("op-block-section-header"));

			Table parameters = table(tr(th("Name")._class("parameter-key"), th("Description")._class("parameter-key")))._class("parameters");

			for (ParameterInfo pi : op.getParameters()) {
				String piName = "body".equals(pi.getIn()) ? "body" : pi.getName();
				boolean required = pi.getRequired() == null ? false : pi.getRequired();

				Td parameterKey = td(
					div(piName)._class("name" + (required ? " required" : "")),
					required ? div("required")._class("requiredlabel") : null,
					div(pi.getType())._class("type"),
					div('(' + pi.getIn() + ')')._class("in")
				)._class("parameter-key");

				Td parameterValue = td(
					div(toBRL(pi.getDescription()))._class("description"),
					examples(s, pi)
				)._class("parameter-value");

				parameters.child(tr(parameterKey, parameterValue));
			}

			tableContainer.child(parameters);
		}

		if (op.hasResponses()) {
			tableContainer.child(div(h4("Responses")._class("title"))._class("op-block-section-header"));

			Table responses = table(tr(th("Code")._class("response-key"), th("Description")._class("response-key")))._class("responses");
			tableContainer.child(responses);

			for (Map.Entry<String,ResponseInfo> e3 : op.getResponses().entrySet()) {
				ResponseInfo ri = e3.getValue();

				Td code = td(e3.getKey())._class("response-key");

				Td codeValue = td(
					div(toBRL(ri.getDescription()))._class("description"),
					examples(s, ri),
					headers(s, ri)
				)._class("response-value");

				responses.child(tr(code, codeValue));
			}
		}

		return tableContainer;
	}

	private Div headers(Session s, ResponseInfo ri) {
		if (! ri.hasHeaders())
			return null;

		Table sectionTable = table(tr(th("Name"),th("Description"),th("Schema")))._class("section-table");

		Div headers = div(
			div("Headers:")._class("section-name"),
			sectionTable
		)._class("headers");

		for (Map.Entry<String,HeaderInfo> e : ri.getHeaders().entrySet()) {
			String name = e.getKey();
			HeaderInfo hi = e.getValue();
			sectionTable.child(
				tr(
					td(name)._class("name"),
					td(toBRL(hi.getDescription()))._class("description"),
					td(hi.asMap().keepAll("type","format","items","collectionFormat","default","maximum","exclusiveMaximum","minimum","exclusiveMinimum","maxLength","minLength","pattern","maxItems","minItems","uniqueItems","enum","multipleOf"))
				)
			);
		}

		return headers;
	}

	private Div examples(Session s, ParameterInfo pi) {
		boolean isBody = "body".equals(pi.getIn());

		ObjectMap m = new ObjectMap();

		try {
			if (isBody) {
				SchemaInfo si = pi.getSchema();
				if (si != null)
					m.put("model", si.copy().resolveRefs(s.swagger, new ArrayDeque<String>(), s.resolveRefsMaxDepth));
			} else {
				ObjectMap om = pi
					.copy()
					.resolveRefs(s.swagger, new ArrayDeque<String>(), s.resolveRefsMaxDepth)
					.asMap()
					.keepAll("format","pattern","collectionFormat","maximum","minimum","multipleOf","maxLength","minLength","maxItems","minItems","allowEmptyValue","exclusiveMaximum","exclusiveMinimum","uniqueItems","items","default","enum");
				m.put("model", om.isEmpty() ? i("none") : om);
			}

			Map<String,?> examples = pi.getExamples();
			if (examples != null)
				for (Map.Entry<String,?> e : examples.entrySet())
					m.put(e.getKey(), e.getValue());
		} catch (Exception e) {
			e.printStackTrace();
		}

		if (m.isEmpty())
			return null;

		return examplesDiv(m);
	}

	private Div examples(Session s, ResponseInfo ri) {
		SchemaInfo si = ri.getSchema();

		ObjectMap m = new ObjectMap();
		try {
			if (si != null) {
				si = si.copy().resolveRefs(s.swagger, new ArrayDeque<String>(), s.resolveRefsMaxDepth);
				m.put("model", si);
			}

			Map<String,?> examples = ri.getExamples();
			if (examples != null)
				for (Map.Entry<String,?> e : examples.entrySet())
					m.put(e.getKey(), e.getValue());
		} catch (Exception e) {
			e.printStackTrace();
		}

		if (m.isEmpty())
			return null;

		return examplesDiv(m);
	}

	private Div examplesDiv(ObjectMap m) {
		if (m.isEmpty())
			return null;

		Select select = null;
		if (m.size() > 1) {
			select = (Select)select().onchange("selectExample(this)")._class("example-select");
		}

		Div div = div(select)._class("examples");

		if (select != null)
			select.child(option("model","model"));
		div.child(div(m.remove("model"))._class("model active").attr("data-name", "model"));

		for (Map.Entry<String,Object> e : m.entrySet()) {
			if (select != null)
				select.child(option(e.getKey(), e.getKey()));
			div.child(div(e.getValue().toString().replaceAll("\\n", "\n"))._class("example").attr("data-name", e.getKey()));
		}

		return div;
	}

	// Creates the "Model" header.
	private HtmlElement modelsBlockSummary() {
		return div()._class("tag-block-summary").children(span("Models")._class("name")).onclick("toggleTagBlock(this)");
	}

	// Creates the contents under the "Model" header.
	private Div modelsBlockContents(Session s) {
		Div modelBlockContents = div()._class("tag-block-contents");
		for (Map.Entry<String,ObjectMap> e : s.swagger.getDefinitions().entrySet())
			modelBlockContents.child(modelBlock(e.getKey(), e.getValue()));
		return modelBlockContents;
	}

	private Div modelBlock(String modelName, ObjectMap model) {
		return div()._class("op-block op-block-closed model").children(
			modelBlockSummary(modelName, model),
			div(model)._class("op-block-contents")
		);
	}

	private HtmlElement modelBlockSummary(String modelName, ObjectMap model) {
		return div()._class("op-block-summary").children(
			span(modelName)._class("method-button"),
			model.containsKey("description") ? span(toBRL(model.remove("description").toString()))._class("summary") : null
		).onclick("toggleOpBlock(this)");
	}

	/**
	 * Replaces newlines with <br> elements.
	 */
	private static List<Object> toBRL(String s) {
		if (s == null)
			return null;
		if (s.indexOf(',') == -1)
			return Collections.<Object>singletonList(s);
		List<Object> l = new ArrayList<>();
		String[] sa = s.split("\n");
		for (int i = 0; i < sa.length; i++) {
			if (i > 0)
				l.add(br());
			l.add(sa[i]);
		}
		return l;
	}
}

