/*
 * Copyright 2017 Google Inc.
 *
 * Licensed 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
 *
 *    https://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 com.android.example.spline.view;

import android.content.Context;
import android.databinding.InverseBindingListener;
import android.graphics.drawable.GradientDrawable;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;

import com.android.example.spline.R;
import com.android.example.spline.model.Color;

/**
 * Two-dimensional selector that allows the user to select a hue and saturation value with touch
 */
public class SaturationValuePicker extends RelativeLayout {

    private static final int SELECTOR_PADDING_DP = 16;
    private static final int SELECTOR_RADIUS_DP = 12;
    private static final int SELECTOR_ELEVATION_DP = 6;

    private float mDensity;
    private float mSelectorRadius;
    private float mInset;
    private int mPadding;

    private float mWidth;
    private float mHeight;
    private Color mColor;
    private float mHue;
    private float mSaturation;
    private float mValue;
    private SaturationValueDrawable mSVDrawable;
    private View mSelector;
    private MarginLayoutParams mSelectorParams;
    private GradientDrawable mSelectorDrawable;
    private InverseBindingListener mSaturationAttrChangedListener;
    private InverseBindingListener mValueAttrChangedListener;

    public SaturationValuePicker(Context context, AttributeSet attrs) {
        super(context, attrs);
        mColor = new Color();

        setClipChildren(false);
        setClipToPadding(false);

        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        mDensity = metrics.density;

        mSelectorRadius = SELECTOR_RADIUS_DP * mDensity;

        View v = new View(context);
        v.setLayoutParams(new LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
        ));

        // We want the selector to protrude as minimally as possible from the selection drawable and
        // we want to inset the gradients in the drawable so that the color under the selector's
        // midpoint always reflects the current color (thus the limits of the saturation-value
        // gradients have to be clamped at this inset. The inset value is thus the shorter side of
        // a 45 degree triangle (1 / root 2) with hypotnus equal to the radius of the selector.
        mInset = mSelectorRadius / (float) Math.sqrt(2);
        mSVDrawable = new SaturationValueDrawable(mInset);
        v.setBackground(mSVDrawable);
        addView(v);

        mSelectorParams = new LayoutParams(
                Math.round(mSelectorRadius * 2), Math.round(mSelectorRadius * 2)
        );
        mSelector = new View(context);
        mSelector.setLayoutParams(mSelectorParams);
        mSelectorDrawable = (GradientDrawable) ContextCompat.getDrawable(
                context, R.drawable.drawable_selector);
        mSelector.setBackground(mSelectorDrawable);
        mSelector.setElevation(SELECTOR_ELEVATION_DP * mDensity);
        addView(mSelector);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
        mPadding = Math.round(SELECTOR_PADDING_DP * mDensity);

        // If we're attached and the parent supports margins, add padding to the view and reverse it
        // with negative margins, allowing room for the selector to protrude from the drawable.
        if (params != null) {
            setPadding(mPadding, mPadding, mPadding, mPadding);
            params.setMargins(-mPadding, -mPadding, -mPadding, -mPadding);
            setLayoutParams(params);
        }
    }

    @Override
    public void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (changed) {
            mWidth = Math.round(getWidth() - (mPadding + mInset) * 2);
            mHeight = Math.round(getHeight() - (mPadding + mInset) * 2);
            updateSelectorPos();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX() - mPadding;
        float y = event.getY() - mPadding;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // Reject touches in the padding region
                if (x < 0 || y < 0 || x > mWidth + mInset * 2 || y > mHeight + mInset * 2) {
                    return false;
                }
            case MotionEvent.ACTION_MOVE:
                setSaturation(getSaturationForX(x - mInset));
                setValue(getValueForY(y - mInset));
                break;
        }

        return true;
    }

    public void setHue(float hue) {
        if (hue != mHue) {
            mHue = hue;
            mColor.setHue(hue);
            mSVDrawable.setHue(hue);
            updateSelectorColor();
        }
    }

    public float getSaturation() {
        return mSaturation;
    }

    public void setSaturation(float saturation) {
        if (saturation != mSaturation) {
            mSaturation = saturation;
            mColor.setSaturation(saturation);
            updateSelectorColor();
            updateSelectorX();
            if (mSaturationAttrChangedListener != null) {
                mSaturationAttrChangedListener.onChange();
            }
        }
    }

    public float getValue() {
        return mValue;
    }

    public void setValue(float value) {
        if (value != mValue) {
            mValue = value;
            mColor.setValue(mValue);
            updateSelectorColor();
            updateSelectorY();
            if (mValueAttrChangedListener != null) {
                mValueAttrChangedListener.onChange();
            }
        }
    }

    private void updateSelectorColor() {
        mSelectorDrawable.setColor(mColor.getColor());
    }

    private void updateSelectorPos() {
        updateSelectorX();
        updateSelectorY();
    }

    private float getXForSaturation(float saturation) {
        return mWidth * saturation;
    }

    private float getSaturationForX(float x) {
        return x / mWidth;
    }

    private float getYForValue(float value) {
        return mHeight * (1.0f - value);
    }

    private float getValueForY(float y) {
        return 1.0f - (y / mHeight);
    }

    private void updateSelectorX() {
        float x = getXForSaturation(mSaturation) + mInset - mSelectorRadius;
        mSelector.setTranslationX(x);
    }

    private void updateSelectorY() {
        float y = getYForValue(mValue) + mInset - mSelectorRadius;
        mSelector.setTranslationY(y);
    }

    public void setSaturationAttrChanged(InverseBindingListener listener) {
        mSaturationAttrChangedListener = listener;
    }

    public void setValueAttrChanged(InverseBindingListener listener) {
        mValueAttrChangedListener = listener;
    }
}