package trikita.anvil;

import android.animation.Animator;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RadioGroup;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextSwitcher;
import android.widget.TextView;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

public class BaseDSL implements Anvil.AttributeSetter {

    static {
        Anvil.registerAttributeSetter(new BaseDSL());
    }

    //
    // Common attrs
    //

    // weight constants
    public final static int FILL = ViewGroup.LayoutParams.MATCH_PARENT;
    public final static int MATCH = ViewGroup.LayoutParams.MATCH_PARENT;
    public final static int WRAP = ViewGroup.LayoutParams.WRAP_CONTENT;

    // gravity constants
    public final static int TOP = Gravity.TOP;
    public final static int BOTTOM = Gravity.BOTTOM;
    public final static int LEFT = Gravity.LEFT;
    public final static int RIGHT = Gravity.RIGHT;
    public final static int CENTER_VERTICAL = Gravity.CENTER_VERTICAL;
    public final static int GROW_VERTICAL = Gravity.FILL_VERTICAL;
    public final static int CENTER_HORIZONTAL = Gravity.CENTER_HORIZONTAL;
    public final static int GROW_HORIZONTAL = Gravity.FILL_HORIZONTAL;
    public final static int CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL;
    public final static int GROW = GROW_VERTICAL | GROW_HORIZONTAL;
    public final static int CLIP_VERTICAL = Gravity.CLIP_VERTICAL;
    public final static int CLIP_HORIZONTAL = Gravity.CLIP_HORIZONTAL;
    public final static int START = Gravity.START;
    public final static int END = Gravity.END;

    //
    // Helper DSL methods, not related to any specific attributes
    //

    public static final class ViewClassResult {
    }

    public static ViewClassResult skip() {
        Anvil.currentMount().iterator.skip();
        return null;
    }

    public static ViewClassResult v(Class<? extends View> c) {
        Anvil.currentMount().iterator.start(c, 0, null);
        return null;
    }

    public static ViewClassResult xml(int layoutId) {
        Anvil.currentMount().iterator.start(null, layoutId, null);
        return null;
    }

    private static Void end() {
        Anvil.currentMount().iterator.end();
        return null;
    }

    public static Void o(ViewClassResult c, Object... args) {
        return end();
    }

    public static Void v(Class<? extends View> c, Anvil.Renderable r) {
        v(c);
        r.view();
        return end();
    }

    public static Void xml(int layoutId, Anvil.Renderable r) {
        xml(layoutId);
        r.view();
        return end();
    }

    public static <T> Void attr(String name, T value) {
        Anvil.currentMount().iterator.attr(name, value);
        return null;
    }

    public static View withId(int id, Anvil.Renderable r) {
        View v = Anvil.currentView();
        if (v == null) {
            throw new RuntimeException("Anvil.currentView() is null");
        }
        // FIXME: how to mock this for tests?
        v = v.findViewById(id);
        if (v == null) {
            throw new RuntimeException("No view found for ID " + id);
        }
        return Anvil.mount(v, r);
    }


    public static Resources R() {
        return Anvil.currentView().getResources();
    }

    public static boolean isPortrait() {
        return R().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
    }

    public static float dip(float value) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value,
                R().getDisplayMetrics());
    }

    public static int dip(int value) {
        return Math.round(dip((float) value));
    }

    public static float sip(float value) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, value,
                R().getDisplayMetrics());
    }

    public static int sip(int value) {
        return Math.round(sip((float) value));
    }

    //
    // Attribute setters
    //

    public static Void size(int w, int h) {
        return attr("size", new AbstractMap.SimpleImmutableEntry<>(w, h));
    }

    public static Void padding(int p) {
        return padding(p, p, p, p);
    }

    public static Void padding(int h, int v) {
        return padding(h, v, h, v);
    }

    public static Void padding(int l, int t, int r, int b) {
        List<Integer> list = new ArrayList<>(4);
        list.add(l);
        list.add(t);
        list.add(r);
        list.add(b);
        return attr("padding", list);
    }

    public static Void margin(int w) {
        return margin(w, w, w, w);
    }

    public static Void margin(int h, int v) {
        return margin(h, v, h, v);
    }

    public static Void margin(int l, int t, int r, int b) {
        List<Integer> list = new ArrayList<>(4);
        list.add(l);
        list.add(t);
        list.add(r);
        list.add(b);
        return attr("margin", list);
    }

    public static Void weight(float w) {
        return attr("weight", w);
    }

    public static Void layoutGravity(int g) {
        return attr("layoutGravity", g);
    }

    public static Void align(int verb) {
        return align(verb, -1);
    }

    public static Void above(int anchor) {
        return align(RelativeLayout.ABOVE, anchor);
    }

    public static Void alignBaseline(int anchor) {
        return align(RelativeLayout.ALIGN_BASELINE, anchor);
    }

    public static Void alignBottom(int anchor) {
        return align(RelativeLayout.ALIGN_BOTTOM, anchor);
    }

    public static Void alignEnd(int anchor) {
        return align(RelativeLayout.ALIGN_END, anchor);
    }

    public static Void alignLeft(int anchor) {
        return align(RelativeLayout.ALIGN_LEFT, anchor);
    }

    public static Void alignParentBottom() {
        return align(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE);
    }

    public static Void alignParentEnd() {
        return align(RelativeLayout.ALIGN_PARENT_END, RelativeLayout.TRUE);
    }

    public static Void alignParentLeft() {
        return align(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
    }

    public static Void alignParentRight() {
        return align(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
    }

    public static Void alignParentStart() {
        return align(RelativeLayout.ALIGN_PARENT_START, RelativeLayout.TRUE);
    }

    public static Void alignParentTop() {
        return align(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
    }

    public static Void alignRight(int anchor) {
        return align(RelativeLayout.ALIGN_RIGHT, anchor);
    }

    public static Void alignStart(int anchor) {
        return align(RelativeLayout.ALIGN_START, anchor);
    }

    public static Void alignTop(int anchor) {
        return align(RelativeLayout.ALIGN_TOP, anchor);
    }

    public static Void below(int anchor) {
        return align(RelativeLayout.BELOW, anchor);
    }

    public static Void centerHorizontal() {
        return align(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE);
    }

    public static Void centerInParent() {
        return align(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
    }

    public static Void centerVertical() {
        return align(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
    }

    public static Void toEndOf(int anchor) {
        return align(RelativeLayout.END_OF, anchor);
    }

    public static Void toLeftOf(int anchor) {
        return align(RelativeLayout.LEFT_OF, anchor);
    }

    public static Void toRightOf(int anchor) {
        return align(RelativeLayout.RIGHT_OF, anchor);
    }

    public static Void toStartOf(int anchor) {
        return align(RelativeLayout.START_OF, anchor);
    }

    public static Void align(int verb, int anchor) {
        return attr("align", new AbstractMap.SimpleImmutableEntry<>(verb, anchor));
    }

    public static Void textSize(float size) {
        return attr("textSize", size);
    }

    public static Void typeface(String font) {
        return attr("typeface", font);
    }

    public static Void typeface(String font, int style) {
        return attr("styledTypeface", new AbstractMap.SimpleImmutableEntry<>(font, style));
    }

    public static Void compoundDrawables(Drawable l, Drawable t, Drawable r, Drawable b) {
        List<Drawable> list = new ArrayList<>(4);
        list.add(l);
        list.add(t);
        list.add(r);
        list.add(b);
        return attr("compoundDrawables", list);
    }

    public static Void compoundDrawablesWithIntrinsicBounds(Drawable l, Drawable t,
                                                            Drawable r, Drawable b) {
        List<Drawable> list = new ArrayList<>(4);
        list.add(l);
        list.add(t);
        list.add(r);
        list.add(b);
        return attr("compoundDrawablesWithIntrinsicBounds", list);
    }

    public static Void compoundDrawablesWithIntrinsicBoundsResource(int l, int t,
                                                                    int r, int b) {
        List<Integer> list = new ArrayList<>(4);
        list.add(l);
        list.add(t);
        list.add(r);
        list.add(b);
        return attr("compoundDrawablesWithIntrinsicBoundsResource", list);
    }

    public static Void visibility(boolean visible) {
        if (visible) {
            return trikita.anvil.DSL.visibility(View.VISIBLE);
        } else {
            return trikita.anvil.DSL.visibility(View.GONE);
        }
    }

    public static Void check(int id) {
        return attr("check", id);
    }

    public static Void tag(int id, Object value) {
        return attr("tag", new AbstractMap.SimpleImmutableEntry<>(id, value));
    }

    public static Void shadowLayer(float radius, float dx, float dy, int color) {
        List<Number> list = new ArrayList<>(4);
        list.add(radius);
        list.add(dx);
        list.add(dy);
        list.add(color);
        return attr("shadowLayer", list);
    }

    public static Void onItemSelected(SimpleItemSelectedListener l) {
        return attr("onItemSelected", l);
    }

    public static Void onSeekBarChange(SimpleSeekBarChangeListener l) {
        return attr("onSeekBarChange", l);
    }

    public static Void anim(boolean trigger, Animator a) {
        return attr("anim", new AnimatorPair(a, trigger));
    }

    public static Void init(Runnable r) {
        return attr("init", r);
    }

    public static Void onTextChanged(SimpleTextWatcher w) {
        return attr("onTextChanged", w);
    }

    public static Void onTextChanged(TextWatcher w) {
        return attr("onTextChanged", w);
    }

    public static Void text(CharSequence arg) {
        return attr("text", arg);
    }


    //
    // Simple lambda-friendly interface wrappers for data bindings
    //

    public interface SimpleTextWatcher {
        void onTextChanged(CharSequence s);
    }

    public interface SimpleSeekBarChangeListener {
        void onSeekBarChange(SeekBar s, int progress, boolean fromUser);
    }

    public interface SimpleItemSelectedListener {
        void onItemSelected(AdapterView a, View v, int pos, long id);
    }

    //
    // Helper classes for data bindings
    //

    private static class TextWatcherProxy implements TextWatcher {

        private final static Map<TextWatcherProxy, Void> TEXT_WATCHERS = new WeakHashMap<>();
        private static TextView CURRENT_INPUT_TEXT_VIEW = null;

        private final TextView v;
        private TextWatcher watcher;
        private SimpleTextWatcher simpleWatcher;
        private String text = "";

        public TextWatcherProxy(TextView v) {
            this.v = v;
        }

        public TextWatcherProxy setImpl(TextWatcher w) {
            this.watcher = w;
            this.simpleWatcher = null;
            return this;
        }

        public TextWatcherProxy setImpl(SimpleTextWatcher w) {
            this.simpleWatcher = w;
            this.watcher = null;
            return this;
        }

        public boolean hasImpl(Object o) {
            if (o == null) {
                return false;
            }
            return o.equals(this.watcher) || o.equals(this.simpleWatcher);
        }

        public void afterTextChanged(Editable s) {
            if (this.watcher != null) {
                this.watcher.afterTextChanged(s);
            }
        }

        public void beforeTextChanged(CharSequence s, int from, int n, int after) {
            if (this.watcher != null) {
                this.watcher.beforeTextChanged(s, from, n, after);
            }
        }

        public void onTextChanged(CharSequence s, int from, int before, int n) {
            TextView old = CURRENT_INPUT_TEXT_VIEW;
            CURRENT_INPUT_TEXT_VIEW = this.v;
            if (this.text.equals(s.toString()) == false) {
                if (this.watcher != null) {
                    this.watcher.onTextChanged(s, from, before, n);
                }
                if (this.simpleWatcher != null) {
                    this.simpleWatcher.onTextChanged(s);
                }
                this.text = s.toString();

                Anvil.render();
            }
            CURRENT_INPUT_TEXT_VIEW = old;
        }
    }

    private final static class ItemSelectedWrapper implements AdapterView.OnItemSelectedListener {
        private final SimpleItemSelectedListener parent;

        private ItemSelectedWrapper(SimpleItemSelectedListener parent) {
            this.parent = parent;
        }

        public void onItemSelected(AdapterView a0, View a1, int a2, long a3) {
            parent.onItemSelected(a0, a1, a2, a3);
            Anvil.render();
        }

        public void onNothingSelected(AdapterView a0) {
        }

        public int hashCode() {
            return parent.hashCode();
        }

        public boolean equals(Object o) {
            return parent.equals(o);
        }
    }

    private final static class SeekBarChangeWrapper implements SeekBar.OnSeekBarChangeListener {
        private final SimpleSeekBarChangeListener parent;

        private SeekBarChangeWrapper(SimpleSeekBarChangeListener parent) {
            this.parent = parent;
        }

        public void onProgressChanged(SeekBar a0, int a1, boolean a2) {
            parent.onSeekBarChange(a0, a1, a2);
            Anvil.render();
        }

        public void onStartTrackingTouch(SeekBar a0) {
        }

        public void onStopTrackingTouch(SeekBar a0) {
        }

        public int hashCode() {
            return parent.hashCode();
        }

        public boolean equals(Object o) {
            return parent.equals(o);
        }
    }

    private final static class AnimatorPair {
        public Animator animator;
        public final boolean trigger;

        public AnimatorPair(Animator a, boolean trigger) {
            this.animator = a;
            this.trigger = trigger;
        }

        public int hashCode() {
            return trigger ? 0 : 1;
        }

        public boolean equals(Object o) {
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            AnimatorPair pair = (AnimatorPair) o;
            if (pair.trigger != this.trigger) {
                if (pair.animator != null && pair.animator.isRunning()) {
                    pair.animator.cancel();
                }
                return false;
            }
            this.animator = pair.animator;
            return true;
        }
    }

    @Override
    public boolean set(View v, String name, Object value, Object prevValue) {
        switch (name) {
            case "init":
                if (Anvil.get(v, "_initialized") == null) {
                    Anvil.set(v, "_initialized", true);
                    ((Runnable) value).run();
                }
                return true;
            case "anim":
                AnimatorPair a = (AnimatorPair) value;
                if (a.trigger) {
                    a.animator.setTarget(v);
                    a.animator.start();
                }
                return true;
            case "onSeekBarChange":
                if (v instanceof SeekBar && value instanceof SimpleSeekBarChangeListener) {
                    ((SeekBar) v).setOnSeekBarChangeListener(
                            new SeekBarChangeWrapper((SimpleSeekBarChangeListener) value));
                    return true;
                }
                break;
            case "onItemSelected":
                if (v instanceof AdapterView && value instanceof SimpleItemSelectedListener) {
                    ((AdapterView) v).setOnItemSelectedListener(new ItemSelectedWrapper((SimpleItemSelectedListener) value));
                    return true;
                } else if (v instanceof AutoCompleteTextView && value instanceof SimpleItemSelectedListener) {
                    ((AutoCompleteTextView) v).setOnItemSelectedListener(new ItemSelectedWrapper((SimpleItemSelectedListener) value));
                    return true;
                }
                break;
            case "text":
                if (v instanceof TextView && value instanceof CharSequence) {
                    if (v != TextWatcherProxy.CURRENT_INPUT_TEXT_VIEW) {
                        ((TextView) v).setText((CharSequence) value);
                    }
                    return true;
                } else if (v instanceof TextSwitcher && value instanceof CharSequence) {
                    ((TextSwitcher) v).setText((CharSequence) value);
                    return true;
                }
                break;
            case "onTextChanged":
                if (v instanceof TextView && value instanceof SimpleTextWatcher) {
                    TextView tv = (TextView) v;
                    for (TextWatcherProxy proxy : TextWatcherProxy.TEXT_WATCHERS.keySet()) {
                        if (proxy.hasImpl(prevValue)) {
                            proxy.setImpl((SimpleTextWatcher) value);
                            return true;
                        }
                    }
                    TextWatcherProxy proxy = new TextWatcherProxy(tv).setImpl((SimpleTextWatcher) value);
                    TextWatcherProxy.TEXT_WATCHERS.put(proxy, null);
                    tv.addTextChangedListener(proxy);
                    return true;
                }
                if (v instanceof TextView && value instanceof TextWatcher) {
                    TextView tv = (TextView) v;
                    for (TextWatcherProxy proxy : TextWatcherProxy.TEXT_WATCHERS.keySet()) {
                        if (proxy.hasImpl(prevValue)) {
                            proxy.setImpl((TextWatcher) value);
                            return true;
                        }
                    }
                    TextWatcherProxy proxy = new TextWatcherProxy(tv).setImpl((TextWatcher) value);
                    TextWatcherProxy.TEXT_WATCHERS.put(proxy, null);
                    tv.addTextChangedListener(proxy);
                    return true;
                }
                break;
            case "tag":
                if (value instanceof AbstractMap.SimpleImmutableEntry) {
                    AbstractMap.SimpleImmutableEntry p = (AbstractMap.SimpleImmutableEntry) value;
                    v.setTag((Integer) p.getKey(), p.getValue());
                    return true;
                }
                break;
            case "size":
                if (value instanceof AbstractMap.SimpleImmutableEntry) {
                    AbstractMap.SimpleImmutableEntry arg = (AbstractMap.SimpleImmutableEntry) value;
                    ViewGroup.LayoutParams p = v.getLayoutParams();
                    p.width = (int) arg.getKey();
                    p.height = (int) arg.getValue();
                    v.setLayoutParams(p);
                    return true;
                }
                break;
            case "check":
                if (v instanceof RadioGroup) {
                    ((RadioGroup) v).check((Integer) value);
                    return true;
                }
                break;
            case "shadowLayer":
                if (v instanceof TextView && value instanceof List) {
                    List<Number> arg = (List<Number>) value;
                    ((TextView) v).setShadowLayer(arg.get(0).floatValue(),
                            arg.get(1).floatValue(), arg.get(2).floatValue(),
                            arg.get(3).intValue());
                    return true;
                }
                break;
            case "padding":
                if (value instanceof List) {
                    List<Integer> arg = (List<Integer>) value;
                    v.setPadding(arg.get(0), arg.get(1), arg.get(2), arg.get(3));
                    return true;
                }
                break;
            case "margin":
                if (v.getLayoutParams() instanceof ViewGroup.MarginLayoutParams && value instanceof List) {
                    List<Integer> arg = (List<Integer>) value;
                    ViewGroup.MarginLayoutParams mp = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
                    mp.leftMargin = arg.get(0);
                    mp.topMargin = arg.get(1);
                    mp.rightMargin = arg.get(2);
                    mp.bottomMargin = arg.get(3);
                    v.setLayoutParams(mp);
                    return true;
                }
                break;
            case "weight":
                if (v.getLayoutParams() instanceof LinearLayout.LayoutParams && value instanceof Float) {
                    ((LinearLayout.LayoutParams) v.getLayoutParams()).weight = (float) value;
                    return true;
                }
                break;
            case "layoutGravity":
                if (v.getLayoutParams() instanceof LinearLayout.LayoutParams && value instanceof Integer) {
                    ((LinearLayout.LayoutParams) v.getLayoutParams()).gravity = (int) value;
                    return true;
                } else if (v.getLayoutParams() instanceof FrameLayout.LayoutParams && value instanceof Integer) {
                    ((FrameLayout.LayoutParams) v.getLayoutParams()).gravity = (int) value;
                    return true;
                }
                break;
            case "align":
                if (v.getLayoutParams() instanceof RelativeLayout.LayoutParams) {
                    AbstractMap.SimpleImmutableEntry<Integer, Integer> e = (AbstractMap.SimpleImmutableEntry<Integer, Integer>) value;
                    ((RelativeLayout.LayoutParams) v.getLayoutParams()).addRule(e.getKey(), e.getValue());
                    return true;
                }
                break;
            case "textSize":
                if (v instanceof TextView && value instanceof Float) {
                    ((TextView) v).setTextSize(TypedValue.COMPLEX_UNIT_PX, (Float) value);
                    return true;
                }
                break;
            case "typeface":
                if (v instanceof TextView && value instanceof String) {
                    ((TextView) v).setTypeface(Typeface.createFromAsset(v.getContext().getAssets(), (String) value));
                    return true;
                }
                break;
            case "styledTypeface":
                if (v instanceof TextView && value instanceof AbstractMap.SimpleImmutableEntry) {
                    AbstractMap.SimpleImmutableEntry<String, Integer> e =
                            (AbstractMap.SimpleImmutableEntry<String, Integer>) value;
                    Typeface typeface = null;
                    if (e.getKey() != null) {
                        typeface = Typeface.createFromAsset(v.getContext().getAssets(), e.getKey());
                    }
                    ((TextView) v).setTypeface(typeface, e.getValue());
                    return true;
                }
                break;
            case "compoundDrawables":
                if (v instanceof TextView && value instanceof List) {
                    List<Drawable> l = (List<Drawable>) value;
                    ((TextView) v).setCompoundDrawables(l.get(0), l.get(1), l.get(2), l.get(3));
                    return true;
                }
                break;
            case "compoundDrawablesWithIntrinsicBounds":
                if (v instanceof TextView && value instanceof List) {
                    List<Drawable> l = (List<Drawable>) value;
                    ((TextView) v).setCompoundDrawablesWithIntrinsicBounds(l.get(0), l.get(1), l.get(2), l.get(3));
                    return true;
                }
                break;
            case "compoundDrawablesWithIntrinsicBoundsResource":
                if (v instanceof TextView && value instanceof List) {
                    List<Integer> l = (List<Integer>) value;
                    ((TextView) v).setCompoundDrawablesWithIntrinsicBounds(l.get(0), l.get(1), l.get(2), l.get(3));
                    return true;
                }
                break;
        }
        return false;
    }
}
