package ru.yandex.webmaster3.api.http.rest.routing;

import ru.yandex.webmaster3.core.util.functional.Bijection;
import ru.yandex.webmaster3.core.util.functional.Functions;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * @author avhaliullin
 */
public abstract class ResourceLocatorBuilder<Locator> {
    protected final Class<Locator> locatorClass;
    protected final List<PathPart<Locator>> path;
    protected final List<QueryParam<Locator, ?>> query;

    protected static <X> List<X> appendElement(List<X> list, X e) {
        List<X> res = new ArrayList<X>(list.size() + 1);
        res.addAll(list);
        res.add(e);
        return res;
    }

    protected QueryString locator2QueryString(Locator loc) {
        List<String> resultPath = new ArrayList<>();
        Map<String, List<String>> resultQuery = new HashMap<>();
        for (PathPart<Locator> pathPart : path) {
            resultPath.add(pathPart.fromLocator(loc));
        }
        for (QueryParam<Locator, ?> queryParam : query) {
            Optional<String> result = queryParam.valueFromLocator(loc);
            if (result.isPresent()) {
                resultQuery.put(queryParam.getName(), Collections.singletonList(result.get()));
            }
        }
        return new QueryString(resultPath, resultQuery);
    }

    public ResourceLocatorBuilder(Class<Locator> locatorClass, List<PathPart<Locator>> path, List<QueryParam<Locator, ?>> query) {
        this.locatorClass = locatorClass;
        this.path = path;
        this.query = query;
    }

    public static abstract class AnyBuilder0<T> extends ResourceLocatorBuilder<T> {
        public AnyBuilder0(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query) {
            super(lc, path, query);
        }

        public <NP> QueryBuilder1<NP, T> queryParam(String name, UrlParam<NP> param, Function<T, NP> mapper, String description) {
            return new QueryBuilder1<>(locatorClass, path, appendElement(query, new QueryParam<>(name, param, l -> Optional.of(mapper.apply(l)), description, true)), ParamExtractor.queryParam(param, name));
        }

        public <NP> QueryBuilder1<Optional<NP>, T> queryOptParam(String name, UrlParam<NP> param, Function<T, Optional<NP>> mapper, String description) {
            return new QueryBuilder1<>(locatorClass, path, appendElement(query, new QueryParam<>(name, param, mapper, description, false)), ParamExtractor.queryOptionalParam(param, name));
        }

        public ResourceBuilder<T> build(Supplier<T> locatorFactory) {
            return new ResourceBuilder<T>(locatorClass, Bijection.create(qs -> locatorFactory.get(), this::locator2QueryString), path, query);
        }
    }

    public static class PathBuilder0<T> extends AnyBuilder0<T> {
        public PathBuilder0(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query) {
            super(lc, path, query);
        }

        public <NP> PathBuilder1<NP, T> pathParam(String name, UrlParam<NP> param, Function<T, NP> mapper, String description) {
            return new PathBuilder1<>(locatorClass, appendElement(path, new PathPart.Param<>(param, name, mapper, description)), query, ParamExtractor.pathParam(param, name, path.size()));
        }

        public PathBuilder0<T> path(String segment) {
            return new PathBuilder0<T>(locatorClass, appendElement(path, new PathPart.Const<T>(segment)), query);
        }
    }

    /* Auto generated classes */

    public static abstract class AnyBuilder1<P1, T> extends ResourceLocatorBuilder<T> {
        protected final ParamExtractor<P1> p1;

        public AnyBuilder1(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query, ParamExtractor<P1> p1) {
            super(lc, path, query);
            this.p1 = p1;
        }

        public ResourceBuilder<T> build(Functions.F1<P1, T> locatorFactory) {
            return new ResourceBuilder<T>(locatorClass, Bijection.create(qs -> locatorFactory.apply(p1.extract(qs)), this::locator2QueryString), path, query);
        }

        public <NP> QueryBuilder2<P1, NP, T> queryParam(String name, UrlParam<NP> param, Function<T, NP> mapper, String description) {
            return new QueryBuilder2<>(locatorClass, path, appendElement(query, new QueryParam<>(name, param, l -> Optional.of(mapper.apply(l)), description, true)), p1, ParamExtractor.queryParam(param, name));
        }

        public <NP> QueryBuilder2<P1, Optional<NP>, T> queryOptParam(String name, UrlParam<NP> param, Function<T, Optional<NP>> mapper, String description) {
            return new QueryBuilder2<>(locatorClass, path, appendElement(query, new QueryParam<>(name, param, mapper, description, false)), p1, ParamExtractor.queryOptionalParam(param, name));
        }
    }

    public static abstract class AnyBuilder2<P1, P2, T> extends ResourceLocatorBuilder<T> {
        protected final ParamExtractor<P1> p1;
        protected final ParamExtractor<P2> p2;

        public AnyBuilder2(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query, ParamExtractor<P1> p1, ParamExtractor<P2> p2) {
            super(lc, path, query);
            this.p1 = p1;
            this.p2 = p2;
        }

        public ResourceBuilder<T> build(Functions.F2<P1, P2, T> locatorFactory) {
            return new ResourceBuilder<T>(locatorClass, Bijection.create(qs -> locatorFactory.apply(p1.extract(qs), p2.extract(qs)), this::locator2QueryString), path, query);
        }

        public <NP> QueryBuilder3<P1, P2, NP, T> queryParam(String name, UrlParam<NP> param, Function<T, NP> mapper, String description) {
            return new QueryBuilder3<>(locatorClass, path, appendElement(query, new QueryParam<>(name, param, l -> Optional.of(mapper.apply(l)), description, true)), p1, p2, ParamExtractor.queryParam(param, name));
        }

        public <NP> QueryBuilder3<P1, P2, Optional<NP>, T> queryOptParam(String name, UrlParam<NP> param, Function<T, Optional<NP>> mapper, String description) {
            return new QueryBuilder3<>(locatorClass, path, appendElement(query, new QueryParam<>(name, param, mapper, description, false)), p1, p2, ParamExtractor.queryOptionalParam(param, name));
        }
    }

    public static abstract class AnyBuilder3<P1, P2, P3, T> extends ResourceLocatorBuilder<T> {
        protected final ParamExtractor<P1> p1;
        protected final ParamExtractor<P2> p2;
        protected final ParamExtractor<P3> p3;

        public AnyBuilder3(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query, ParamExtractor<P1> p1, ParamExtractor<P2> p2, ParamExtractor<P3> p3) {
            super(lc, path, query);
            this.p1 = p1;
            this.p2 = p2;
            this.p3 = p3;
        }

        public ResourceBuilder<T> build(Functions.F3<P1, P2, P3, T> locatorFactory) {
            return new ResourceBuilder<T>(locatorClass, Bijection.create(qs -> locatorFactory.apply(p1.extract(qs), p2.extract(qs), p3.extract(qs)), this::locator2QueryString), path, query);
        }

        public <NP> QueryBuilder4<P1, P2, P3, NP, T> queryParam(String name, UrlParam<NP> param, Function<T, NP> mapper, String description) {
            return new QueryBuilder4<>(locatorClass, path, appendElement(query, new QueryParam<>(name, param, l -> Optional.of(mapper.apply(l)), description, true)), p1, p2, p3, ParamExtractor.queryParam(param, name));
        }

        public <NP> QueryBuilder4<P1, P2, P3, Optional<NP>, T> queryOptParam(String name, UrlParam<NP> param, Function<T, Optional<NP>> mapper, String description) {
            return new QueryBuilder4<>(locatorClass, path, appendElement(query, new QueryParam<>(name, param, mapper, description, false)), p1, p2, p3, ParamExtractor.queryOptionalParam(param, name));
        }
    }

    public static abstract class AnyBuilder4<P1, P2, P3, P4, T> extends ResourceLocatorBuilder<T> {
        protected final ParamExtractor<P1> p1;
        protected final ParamExtractor<P2> p2;
        protected final ParamExtractor<P3> p3;
        protected final ParamExtractor<P4> p4;

        public AnyBuilder4(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query, ParamExtractor<P1> p1, ParamExtractor<P2> p2, ParamExtractor<P3> p3, ParamExtractor<P4> p4) {
            super(lc, path, query);
            this.p1 = p1;
            this.p2 = p2;
            this.p3 = p3;
            this.p4 = p4;
        }

        public ResourceBuilder<T> build(Functions.F4<P1, P2, P3, P4, T> locatorFactory) {
            return new ResourceBuilder<T>(locatorClass, Bijection.create(qs -> locatorFactory.apply(p1.extract(qs), p2.extract(qs), p3.extract(qs), p4.extract(qs)), this::locator2QueryString), path, query);
        }

        public <NP> QueryBuilder5<P1, P2, P3, P4, NP, T> queryParam(String name, UrlParam<NP> param, Function<T, NP> mapper, String description) {
            return new QueryBuilder5<>(locatorClass, path, appendElement(query, new QueryParam<>(name, param, l -> Optional.of(mapper.apply(l)), description, true)), p1, p2, p3, p4, ParamExtractor.queryParam(param, name));
        }

        public <NP> QueryBuilder5<P1, P2, P3, P4, Optional<NP>, T> queryOptParam(String name, UrlParam<NP> param, Function<T, Optional<NP>> mapper, String description) {
            return new QueryBuilder5<>(locatorClass, path, appendElement(query, new QueryParam<>(name, param, mapper, description, false)), p1, p2, p3, p4, ParamExtractor.queryOptionalParam(param, name));
        }
    }

    public static abstract class AnyBuilder5<P1, P2, P3, P4, P5, T> extends ResourceLocatorBuilder<T> {
        protected final ParamExtractor<P1> p1;
        protected final ParamExtractor<P2> p2;
        protected final ParamExtractor<P3> p3;
        protected final ParamExtractor<P4> p4;
        protected final ParamExtractor<P5> p5;

        public AnyBuilder5(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query, ParamExtractor<P1> p1, ParamExtractor<P2> p2, ParamExtractor<P3> p3, ParamExtractor<P4> p4, ParamExtractor<P5> p5) {
            super(lc, path, query);
            this.p1 = p1;
            this.p2 = p2;
            this.p3 = p3;
            this.p4 = p4;
            this.p5 = p5;
        }

        public ResourceBuilder<T> build(Functions.F5<P1, P2, P3, P4, P5, T> locatorFactory) {
            return new ResourceBuilder<T>(locatorClass, Bijection.create(qs -> locatorFactory.apply(p1.extract(qs), p2.extract(qs), p3.extract(qs), p4.extract(qs), p5.extract(qs)), this::locator2QueryString), path, query);
        }
    }

    public static class PathBuilder1<P1, T> extends AnyBuilder1<P1, T> {
        public PathBuilder1(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query, ParamExtractor<P1> p1) {
            super(lc, path, query, p1);
        }

        public PathBuilder1<P1, T> path(String segment) {
            return new PathBuilder1<>(locatorClass, appendElement(path, new PathPart.Const<T>(segment)), query, p1);
        }

        public <NP> PathBuilder2<P1, NP, T> pathParam(String name, UrlParam<NP> param, Function<T, NP> mapper, String description) {
            return new PathBuilder2<>(locatorClass, appendElement(path, new PathPart.Param<>(param, name, mapper, description)), query, p1, ParamExtractor.pathParam(param, name, path.size()));
        }
    }

    public static class PathBuilder2<P1, P2, T> extends AnyBuilder2<P1, P2, T> {
        public PathBuilder2(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query, ParamExtractor<P1> p1, ParamExtractor<P2> p2) {
            super(lc, path, query, p1, p2);
        }

        public PathBuilder2<P1, P2, T> path(String segment) {
            return new PathBuilder2<>(locatorClass, appendElement(path, new PathPart.Const<T>(segment)), query, p1, p2);
        }

        public <NP> PathBuilder3<P1, P2, NP, T> pathParam(String name, UrlParam<NP> param, Function<T, NP> mapper, String description) {
            return new PathBuilder3<>(locatorClass, appendElement(path, new PathPart.Param<>(param, name, mapper, description)), query, p1, p2, ParamExtractor.pathParam(param, name, path.size()));
        }
    }

    public static class PathBuilder3<P1, P2, P3, T> extends AnyBuilder3<P1, P2, P3, T> {
        public PathBuilder3(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query, ParamExtractor<P1> p1, ParamExtractor<P2> p2, ParamExtractor<P3> p3) {
            super(lc, path, query, p1, p2, p3);
        }

        public PathBuilder3<P1, P2, P3, T> path(String segment) {
            return new PathBuilder3<>(locatorClass, appendElement(path, new PathPart.Const<T>(segment)), query, p1, p2, p3);
        }

        public <NP> PathBuilder4<P1, P2, P3, NP, T> pathParam(String name, UrlParam<NP> param, Function<T, NP> mapper, String description) {
            return new PathBuilder4<>(locatorClass, appendElement(path, new PathPart.Param<>(param, name, mapper, description)), query, p1, p2, p3, ParamExtractor.pathParam(param, name, path.size()));
        }
    }

    public static class PathBuilder4<P1, P2, P3, P4, T> extends AnyBuilder4<P1, P2, P3, P4, T> {
        public PathBuilder4(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query, ParamExtractor<P1> p1, ParamExtractor<P2> p2, ParamExtractor<P3> p3, ParamExtractor<P4> p4) {
            super(lc, path, query, p1, p2, p3, p4);
        }

        public PathBuilder4<P1, P2, P3, P4, T> path(String segment) {
            return new PathBuilder4<>(locatorClass, appendElement(path, new PathPart.Const<T>(segment)), query, p1, p2, p3, p4);
        }

        public <NP> PathBuilder5<P1, P2, P3, P4, NP, T> pathParam(String name, UrlParam<NP> param, Function<T, NP> mapper, String description) {
            return new PathBuilder5<>(locatorClass, appendElement(path, new PathPart.Param<>(param, name, mapper, description)), query, p1, p2, p3, p4, ParamExtractor.pathParam(param, name, path.size()));
        }
    }

    public static class PathBuilder5<P1, P2, P3, P4, P5, T> extends AnyBuilder5<P1, P2, P3, P4, P5, T> {
        public PathBuilder5(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query, ParamExtractor<P1> p1, ParamExtractor<P2> p2, ParamExtractor<P3> p3, ParamExtractor<P4> p4, ParamExtractor<P5> p5) {
            super(lc, path, query, p1, p2, p3, p4, p5);
        }

        public PathBuilder5<P1, P2, P3, P4, P5, T> path(String segment) {
            return new PathBuilder5<>(locatorClass, appendElement(path, new PathPart.Const<T>(segment)), query, p1, p2, p3, p4, p5);
        }
    }

    public static class QueryBuilder1<P1, T> extends AnyBuilder1<P1, T> {
        public QueryBuilder1(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query, ParamExtractor<P1> p1) {
            super(lc, path, query, p1);
        }
    }

    public static class QueryBuilder2<P1, P2, T> extends AnyBuilder2<P1, P2, T> {
        public QueryBuilder2(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query, ParamExtractor<P1> p1, ParamExtractor<P2> p2) {
            super(lc, path, query, p1, p2);
        }
    }

    public static class QueryBuilder3<P1, P2, P3, T> extends AnyBuilder3<P1, P2, P3, T> {
        public QueryBuilder3(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query, ParamExtractor<P1> p1, ParamExtractor<P2> p2, ParamExtractor<P3> p3) {
            super(lc, path, query, p1, p2, p3);
        }
    }

    public static class QueryBuilder4<P1, P2, P3, P4, T> extends AnyBuilder4<P1, P2, P3, P4, T> {
        public QueryBuilder4(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query, ParamExtractor<P1> p1, ParamExtractor<P2> p2, ParamExtractor<P3> p3, ParamExtractor<P4> p4) {
            super(lc, path, query, p1, p2, p3, p4);
        }
    }

    public static class QueryBuilder5<P1, P2, P3, P4, P5, T> extends AnyBuilder5<P1, P2, P3, P4, P5, T> {
        public QueryBuilder5(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query, ParamExtractor<P1> p1, ParamExtractor<P2> p2, ParamExtractor<P3> p3, ParamExtractor<P4> p4, ParamExtractor<P5> p5) {
            super(lc, path, query, p1, p2, p3, p4, p5);
        }
    }

    /**
     * Метод для генерации вышележащих builder'ов.
     */
    private static String generateBuilders(int maxPathParams, int maxTotalParams) {
        StringBuilder result = new StringBuilder();
        for (int i = 1; i <= maxTotalParams; i++) {
            boolean isLast = i == maxTotalParams;
            String paramTypesList = "";
            for (int j = 1; j <= i; j++) {
                if (!paramTypesList.isEmpty()) {
                    paramTypesList += ", ";
                }
                paramTypesList += "P" + j;
            }

            String extractorNamesList = paramTypesList.toLowerCase();

            result.append(String.format(
                    "public static abstract class AnyBuilder%1$d<%2$s, T> extends ResourceLocatorBuilder<T> {\n",
                    i, paramTypesList
            ));
            for (int j = 1; j <= i; j++) {
                result.append(String.format("protected final ParamExtractor<P%1$d> p%1$d;\n", j));
            }
            result.append(String.format("public AnyBuilder%1$d(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query", i));
            for (int j = 1; j <= i; j++) {
                result.append(String.format(", ParamExtractor<P%1$d> p%1$d", j));
            }
            result.append(") { \n");
            result.append("super(lc, path, query);\n");
            for (int j = 1; j <= i; j++) {
                result.append(String.format("this.p%1$d = p%1$d;\n", j));
            }
            result.append("}\n");
            result.append(String.format(
                    "public ResourceBuilder<T> build(Functions.F%1$d<%2$s, T> locatorFactory) {\n",
                    i, paramTypesList
            ));
            String applyExtractors = "";
            for (int j = 1; j <= i; j++) {
                if (!applyExtractors.isEmpty()) {
                    applyExtractors += ", ";
                }
                applyExtractors += "p" + j + ".extract(qs)";
            }
            result.append(String.format(
                    "return new ResourceBuilder<T>(locatorClass, Bijection.create(qs -> locatorFactory.apply(%1$s), this::locator2QueryString), path, query);\n",
                    applyExtractors
            ));
            result.append("}\n");

            if (!isLast) {
                result.append(String.format(
                        "public <NP> QueryBuilder%1$d<%2$s, NP, T> queryParam(String name, UrlParam<NP> param, Function<T, NP> mapper, String description) {\n",
                        i + 1, paramTypesList
                ));
                result.append(String.format(
                        "return new QueryBuilder%1$d<>(locatorClass, path, appendElement(query, new QueryParam<>(name, param, l -> Optional.of(mapper.apply(l)), description, true)), %2$s, ParamExtractor.queryParam(param, name));\n",
                        i + 1, extractorNamesList
                ));
                result.append("}\n");

                result.append(String.format(
                        "public <NP> QueryBuilder%1$d<%2$s, Optional<NP>, T> queryOptParam(String name, UrlParam<NP> param, Function<T, Optional<NP>> mapper, String description) {\n",
                        i + 1, paramTypesList
                ));
                result.append(String.format(
                        "return new QueryBuilder%1$d<>(locatorClass, path, appendElement(query, new QueryParam<>(name, param, mapper, description, false)), %2$s, ParamExtractor.queryOptionalParam(param, name));\n",
                        i + 1, extractorNamesList
                ));
                result.append("}\n");
            }
            result.append("}\n");
        }

        for (int i = 1; i <= maxPathParams; i++) {
            boolean isLast = i == maxPathParams;
            String paramTypesList = "";
            for (int j = 1; j <= i; j++) {
                if (!paramTypesList.isEmpty()) {
                    paramTypesList += ", ";
                }
                paramTypesList += "P" + j;
            }
            String extractorNamesList = paramTypesList.toLowerCase();

            result.append(String.format(
                    "public static class PathBuilder%1$d<%2$s, T> extends AnyBuilder%1$d<%2$s, T> {\n",
                    i, paramTypesList
            ));
            result.append(String.format(
                    "public PathBuilder%1$d(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query",
                    i
            ));
            for (int j = 1; j <= i; j++) {
                result.append(String.format(
                        ", ParamExtractor<P%1$d> p%1$d",
                        j
                ));
            }
            result.append(") {\n");
            result.append(String.format(
                    "super(lc, path, query, %1$s);\n",
                    extractorNamesList
            ));
            result.append("}\n");

            result.append(String.format(
                    "public PathBuilder%1$d<%2$s, T> path(String segment) {\n",
                    i, paramTypesList
            ));
            result.append(String.format(
                    "return new PathBuilder%1$d<>(locatorClass, appendElement(path, new PathPart.Const<T>(segment)), query, %2$s);\n",
                    i, extractorNamesList
            ));
            result.append("}\n");
            if (!isLast) {
                result.append(String.format(
                        "public <NP> PathBuilder%1$d<%2$s, NP, T> pathParam(String name, UrlParam<NP> param, Function<T, NP> mapper, String description) {\n",
                        i + 1, paramTypesList
                ));
                result.append(String.format(
                        "return new PathBuilder%1$d<>(locatorClass, appendElement(path, new PathPart.Param<>(param, name, mapper, description)), query, %2$s, ParamExtractor.pathParam(param, name, path.size()));\n",
                        i + 1, extractorNamesList
                ));
                result.append("}\n");
            }
            result.append("}\n");
        }
        for (int i = 1; i <= maxTotalParams; i++) {
            String paramTypesList = "";
            for (int j = 1; j <= i; j++) {
                if (!paramTypesList.isEmpty()) {
                    paramTypesList += ", ";
                }
                paramTypesList += "P" + j;
            }
            String extractorNamesList = paramTypesList.toLowerCase();

            result.append(String.format(
                    "public static class QueryBuilder%1$d<%2$s, T> extends AnyBuilder%1$d<%2$s, T> {\n",
                    i, paramTypesList
            ));
            result.append(String.format(
                    "public QueryBuilder%1$d(Class<T> lc, List<PathPart<T>> path, List<QueryParam<T, ?>> query",
                    i
            ));
            for (int j = 1; j <= i; j++) {
                result.append(String.format(
                        ", ParamExtractor<P%1$d> p%1$d",
                        j
                ));
            }
            result.append(") {\n");
            result.append(String.format(
                    "super(lc, path, query, %1$s);\n",
                    extractorNamesList
            ));
            result.append("}\n");
            result.append("}\n");
        }
        return result.toString();
    }

//    public static void main(String[] args) {
//        System.out.print(generateBuilders(5, 5));
//    }
}
