// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
package com.mojang.datafixers.optics;

import com.mojang.datafixers.FunctionType;
import com.mojang.datafixers.kinds.App;
import com.mojang.datafixers.kinds.App2;
import com.mojang.datafixers.kinds.Applicative;
import com.mojang.datafixers.kinds.K1;
import com.mojang.datafixers.optics.profunctors.AffineP;
import com.mojang.datafixers.optics.profunctors.Cartesian;
import com.mojang.datafixers.optics.profunctors.Cocartesian;
import com.mojang.datafixers.optics.profunctors.GetterP;
import com.mojang.datafixers.optics.profunctors.Profunctor;
import com.mojang.datafixers.optics.profunctors.TraversalP;
import com.mojang.datafixers.util.Either;
import com.mojang.datafixers.util.Pair;

import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

public abstract class Optics {
    public static <S, T, A, B> Adapter<S, T, A, B> toAdapter(final Optic<? super Profunctor.Mu, S, T, A, B> optic) {
        final Function<App2<Adapter.Mu<A, B>, A, B>, App2<Adapter.Mu<A, B>, S, T>> eval = optic.eval(new Adapter.Instance<>());
        return Adapter.unbox(eval.apply(adapter(Function.identity(), Function.identity())));
    }

    public static <S, T, A, B> Lens<S, T, A, B> toLens(final Optic<? super Cartesian.Mu, S, T, A, B> optic) {
        final Function<App2<Lens.Mu<A, B>, A, B>, App2<Lens.Mu<A, B>, S, T>> eval = optic.eval(new Lens.Instance<>());
        return Lens.unbox(eval.apply(lens(Function.identity(), (b, a) -> b)));
    }

    public static <S, T, A, B> Prism<S, T, A, B> toPrism(final Optic<? super Cocartesian.Mu, S, T, A, B> optic) {
        final Function<App2<Prism.Mu<A, B>, A, B>, App2<Prism.Mu<A, B>, S, T>> eval = optic.eval(new Prism.Instance<>());
        return Prism.unbox(eval.apply(prism(Either::right, Function.identity())));
    }

    public static <S, T, A, B> Affine<S, T, A, B> toAffine(final Optic<? super AffineP.Mu, S, T, A, B> optic) {
        final Function<App2<Affine.Mu<A, B>, A, B>, App2<Affine.Mu<A, B>, S, T>> eval = optic.eval(new Affine.Instance<>());
        return Affine.unbox(eval.apply(affine(Either::right, (b, a) -> b)));
    }

    public static <S, T, A, B> Getter<S, T, A, B> toGetter(final Optic<? super GetterP.Mu, S, T, A, B> optic) {
        final Function<App2<Getter.Mu<A, B>, A, B>, App2<Getter.Mu<A, B>, S, T>> eval = optic.eval(new Getter.Instance<>());
        return Getter.unbox(eval.apply(getter(Function.identity())));
    }

    public static <S, T, A, B> Traversal<S, T, A, B> toTraversal(final Optic<? super TraversalP.Mu, S, T, A, B> optic) {
        final Function<App2<Traversal.Mu<A, B>, A, B>, App2<Traversal.Mu<A, B>, S, T>> eval = optic.eval(new Traversal.Instance<>());
        return Traversal.unbox(eval.apply(new Traversal<A, B, A, B>() {
            @Override
            public <F extends K1> FunctionType<A, App<F, B>> wander(final Applicative<F, ?> applicative, final FunctionType<A, App<F, B>> input) {
                return input;
            }
        }));
    }

    static <S, T, A, B, F> Lens<S, T, Pair<F, A>, B> merge(final Lens<S, ?, F, ?> getter, final Lens<S, T, A, B> lens) {
        return lens(
            s -> Pair.of(getter.view(s), lens.view(s)),
            lens::update
        );
    }

    public static <S, T> Adapter<S, T, S, T> id() {
        return new IdAdapter<>();
    }

    public static <S, T, A, B> Adapter<S, T, A, B> adapter(final Function<S, A> from, final Function<B, T> to) {
        return new Adapter<S, T, A, B>() {
            @Override
            public A from(final S s) {
                return from.apply(s);
            }

            @Override
            public T to(final B b) {
                return to.apply(b);
            }
        };
    }

    public static <S, T, A, B> Lens<S, T, A, B> lens(final Function<S, A> view, final BiFunction<B, S, T> update) {
        return new Lens<S, T, A, B>() {
            @Override
            public A view(final S s) {
                return view.apply(s);
            }

            @Override
            public T update(final B b, final S s) {
                return update.apply(b, s);
            }
        };
    }

    public static <S, T, A, B> Prism<S, T, A, B> prism(final Function<S, Either<T, A>> match, final Function<B, T> build) {
        return new Prism<S, T, A, B>() {
            @Override
            public Either<T, A> match(final S s) {
                return match.apply(s);
            }

            @Override
            public T build(final B b) {
                return build.apply(b);
            }
        };
    }

    public static <S, T, A, B> Affine<S, T, A, B> affine(final Function<S, Either<T, A>> preview, final BiFunction<B, S, T> build) {
        return new Affine<S, T, A, B>() {
            @Override
            public Either<T, A> preview(final S s) {
                return preview.apply(s);
            }

            @Override
            public T set(final B b, final S s) {
                return build.apply(b, s);
            }
        };
    }

    public static <S, T, A, B> Getter<S, T, A, B> getter(final Function<S, A> get) {
        return get::apply;
    }

    public static <R, A, B> Forget<R, A, B> forget(final Function<A, R> function) {
        return function::apply;
    }

    public static <R, A, B> ForgetOpt<R, A, B> forgetOpt(final Function<A, Optional<R>> function) {
        return function::apply;
    }

    public static <R, A, B> ForgetE<R, A, B> forgetE(final Function<A, Either<B, R>> function) {
        return function::apply;
    }

    public static <R, A, B> ReForget<R, A, B> reForget(final Function<R, B> function) {
        return function::apply;
    }

    public static <S, T, A, B> Grate<S, T, A, B> grate(final FunctionType<FunctionType<FunctionType<S, A>, B>, T> grate) {
        return grate::apply;
    }

    public static <R, A, B> ReForgetEP<R, A, B> reForgetEP(final String name, final Function<Either<A, Pair<A, R>>, B> function) {
        return new ReForgetEP<R, A, B>() {
            @Override
            public B run(final Either<A, Pair<A, R>> e) {
                return function.apply(e);
            }

            @Override
            public String toString() {
                return "ReForgetEP_" + name;
            }
        };
    }

    public static <R, A, B> ReForgetE<R, A, B> reForgetE(final String name, final Function<Either<A, R>, B> function) {
        return new ReForgetE<R, A, B>() {
            @Override
            public B run(final Either<A, R> t) {
                return function.apply(t);
            }

            @Override
            public String toString() {
                return "ReForgetE_" + name;
            }
        };
    }

    public static <R, A, B> ReForgetP<R, A, B> reForgetP(final String name, final BiFunction<A, R, B> function) {
        return new ReForgetP<R, A, B>() {
            @Override
            public B run(final A a, final R r) {
                return function.apply(a, r);
            }

            @Override
            public String toString() {
                return "ReForgetP_" + name;
            }
        };
    }

    public static <R, A, B> ReForgetC<R, A, B> reForgetC(final String name, final Either<Function<R, B>, BiFunction<A, R, B>> either) {
        return new ReForgetC<R, A, B>() {
            @Override
            public Either<Function<R, B>, BiFunction<A, R, B>> impl() {
                return either;
            }

            @Override
            public String toString() {
                return "ReForgetC_" + name;
            }
        };
    }

    public static <I, J, X> PStore<I, J, X> pStore(final Function<J, X> peek, final Supplier<I> pos) {
        return new PStore<I, J, X>() {
            @Override
            public X peek(final J j) {
                return peek.apply(j);
            }

            @Override
            public I pos() {
                return pos.get();
            }
        };
    }

    public static <A, B> Function<A, B> getFunc(final App2<FunctionType.Mu, A, B> box) {
        return FunctionType.unbox(box);
    }

    public static <F, G, F2> Proj1<F, G, F2> proj1() {
        return new Proj1<>();
    }

    public static <F, G, G2> Proj2<F, G, G2> proj2() {
        return new Proj2<>();
    }

    public static <F, G, F2> Inj1<F, G, F2> inj1() {
        return new Inj1<>();
    }

    public static <F, G, G2> Inj2<F, G, G2> inj2() {
        return new Inj2<>();
    }

    /*public static <Proof extends Cartesian.Mu, S1, S2, T1, T2, A, B> Optic<Proof, Either<S1, S2>, Either<T1, T2>, A, B> choosing(final Optic<? super Profunctor.Mu, S1, T1, A, B> first, final Optic<? super Profunctor.Mu, S2, T2, A, B> second) {
        return new Optic<Proof, Either<S1, S2>, Either<T1, T2>, A, B>() {
            @Override
            public <P extends K2> FunctionType<App2<P, A, B>, App2<P, Either<S1, S2>, Either<T1, T2>>> eval(final App<? extends Proof, P> proof) {
                final Cartesian<? extends Proof> cartesian = Cartesian.unbox(proof);
                final ProfunctorFunctorWrapper.Instance<P, Either.Mu<A>, Either.Mu<B>> i1 = new ProfunctorFunctorWrapper.Instance<>(proof, new Either.Instance<>(), new Either.Instance<>());
                final ProfunctorFunctorWrapper.Instance<P, Either.Mu<S2>, Either.Mu<T2>> i2 = new ProfunctorFunctorWrapper.Instance<>(proof, new Either.Instance<>(), new Either.Instance<>());
                return pab -> {
                    final App2<P, Pair<Boolean, A>, Pair<Boolean, B>> withBool = cartesian.second(pab);
                    final App2<P, App<Either.Mu<A>, A>, App<Either.Mu<B>, B>> either = cartesian.dimap(
                        withBool,
                        e -> Either.unbox(e).map(l -> Pair.of(false, l), r -> Pair.of(true, r)),
                        pair -> pair.getFirst() ? Either.right(pair.getSecond()) : Either.left(pair.getSecond())
                    );
                    final ProfunctorFunctorWrapper<P, Either.Mu<A>, Either.Mu<B>, A, B> wrapper1 = new ProfunctorFunctorWrapper<>(either);
                    final App2<P, App<Either.Mu<A>, S2>, App<Either.Mu<B>, T2>> secondApplied = ProfunctorFunctorWrapper.unbox(second.eval(i1).apply(wrapper1)).value();
                    final App2<P, App<Either.Mu<S2>, A>, App<Either.Mu<T2>, B>> swapped = cartesian.dimap(secondApplied, e -> Either.unbox(e).swap(), e -> Either.unbox(e).swap());
                    final ProfunctorFunctorWrapper<P, Either.Mu<S2>, Either.Mu<T2>, A, B> wrapper2 = new ProfunctorFunctorWrapper<>(swapped);
                    final App2<P, App<Either.Mu<S2>, S1>, App<Either.Mu<T2>, T1>> firstApplied = ProfunctorFunctorWrapper.unbox(first.eval(i2).apply(wrapper2)).value();
                    return cartesian.dimap(firstApplied, e -> e, Either::unbox);
                };
            }
        };
    }*/

    public static <F, G, F2, G2, A, B> Lens<Either<F, G>, Either<F2, G2>, A, B> eitherLens(final Lens<F, F2, A, B> fLens, final Lens<G, G2, A, B> gLens) {
        return lens(
            either -> either.map(fLens::view, gLens::view),
            (b, either) -> either.mapBoth(f -> fLens.update(b, f), g -> gLens.update(b, g))
        );
    }

    public static <F, G, F2, G2, A, B> Affine<Either<F, G>, Either<F2, G2>, A, B> eitherAffine(final Affine<F, F2, A, B> fAffine, final Affine<G, G2, A, B> gAffine) {
        return affine(
            either -> either.map(
                f -> fAffine.preview(f).mapLeft(Either::left),
                g -> gAffine.preview(g).mapLeft(Either::right)
            ),
            (b, either) -> either.mapBoth(f -> fAffine.set(b, f), g -> gAffine.set(b, g))
        );
    }

    public static <F, G, F2, G2, A, B> Traversal<Either<F, G>, Either<F2, G2>, A, B> eitherTraversal(final Traversal<F, F2, A, B> fOptic, final Traversal<G, G2, A, B> gOptic) {
        return new Traversal<Either<F, G>, Either<F2, G2>, A, B>() {
            @Override
            public <FT extends K1> FunctionType<Either<F, G>, App<FT, Either<F2, G2>>> wander(final Applicative<FT, ?> applicative, final FunctionType<A, App<FT, B>> input) {
                return e -> e.map(
                    l -> {
                        return applicative.ap(Either::left, fOptic.wander(applicative, input).apply(l));
                    },
                    r -> {
                        return applicative.ap(Either::right, gOptic.wander(applicative, input).apply(r));
                    }
                );
            }
        };
    }
}