/*
 * Copyright 2013 serso aka se.solovyev
 *
 * 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
 *
 *    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.
 *
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 * Contact details
 *
 * Email: se.solovyev@gmail.com
 * Site:  http://se.solovyev.org
 */

package org.solovyev.android.calculator.floating;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.v7.view.ContextThemeWrapper;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.*;
import android.widget.ImageView;
import org.solovyev.android.calculator.*;
import org.solovyev.android.calculator.buttons.CppButton;
import org.solovyev.android.calculator.keyboard.BaseKeyboardUi;
import org.solovyev.android.views.Adjuster;

import javax.annotation.Nonnull;
import javax.inject.Inject;
import javax.inject.Named;

import static android.view.HapticFeedbackConstants.*;
import static android.view.WindowManager.LayoutParams.*;
import static org.solovyev.android.calculator.App.cast;

public class FloatingCalculatorView {

    private static class MyTouchListener implements View.OnTouchListener {
        private static final float DIST_EPS = 0f;
        private static final float DIST_MAX = 100000f;
        private static final long TIME_EPS = 0L;

        @Nonnull
        private final WindowManager wm;
        @Nonnull
        private final View view;
        private int orientation;
        private float x0;
        private float y0;
        private long lastMoveTime = 0;
        private final DisplayMetrics dm = new DisplayMetrics();

        public MyTouchListener(@Nonnull WindowManager wm,
                @Nonnull View view) {
            this.wm = wm;
            this.view = view;
            onDisplayChanged();
        }

        private void onDisplayChanged() {
            final Display dd = wm.getDefaultDisplay();
            //noinspection deprecation
            orientation = dd.getOrientation();
            dd.getMetrics(dm);
        }

        @SuppressLint("ClickableViewAccessibility")
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            //noinspection deprecation
            if (orientation != wm.getDefaultDisplay().getOrientation()) {
                // orientation has changed => we need to check display width/height each time window moved
                onDisplayChanged();
            }

            final float x1 = event.getRawX();
            final float y1 = event.getRawY();

            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    x0 = x1;
                    y0 = y1;
                    return true;

                case MotionEvent.ACTION_MOVE:
                    final long now = System.currentTimeMillis();
                    if (now - lastMoveTime >= TIME_EPS) {
                        lastMoveTime = now;
                        processMove(x1, y1);
                    }
                    return true;
            }

            return false;
        }

        private void processMove(float x1, float y1) {
            final float Δx = x1 - x0;
            final float Δy = y1 - y0;

            final WindowManager.LayoutParams params =
                    (WindowManager.LayoutParams) view.getLayoutParams();

            boolean xInBounds = isDistanceInBounds(Δx);
            boolean yInBounds = isDistanceInBounds(Δy);
            if (xInBounds || yInBounds) {

                if (xInBounds) {
                    params.x = (int) (params.x + Δx);
                }

                if (yInBounds) {
                    params.y = (int) (params.y + Δy);
                }

                params.x = Math.min(Math.max(params.x, 0), dm.widthPixels - params.width);
                params.y = Math.min(Math.max(params.y, 0), dm.heightPixels - params.height);

                wm.updateViewLayout(view, params);

                if (xInBounds) {
                    x0 = x1;
                }

                if (yInBounds) {
                    y0 = y1;
                }
            }
        }

        private boolean isDistanceInBounds(float δx) {
            δx = Math.abs(δx);
            return δx >= DIST_EPS && δx < DIST_MAX;
        }
    }

    public static class State implements Parcelable {

        public static final Creator<State> CREATOR = new Creator<State>() {
            public State createFromParcel(@Nonnull Parcel in) {
                return new State(in);
            }

            public State[] newArray(int size) {
                return new State[size];
            }
        };
        public final int width;
        public final int height;
        public final int x;
        public final int y;

        public State(int width, int height, int x, int y) {
            this.width = width;
            this.height = height;
            this.x = x;
            this.y = y;
        }

        private State(@NonNull SharedPreferences prefs) {
            width = prefs.getInt("width", 200);
            height = prefs.getInt("height", 400);
            x = prefs.getInt("x", 0);
            y = prefs.getInt("y", 0);
        }

        public State(@Nonnull Parcel in) {
            width = in.readInt();
            height = in.readInt();
            x = in.readInt();
            y = in.readInt();
        }

        @android.support.annotation.Nullable
        public static State fromPrefs(@NonNull SharedPreferences prefs) {
            if(!prefs.contains("width")) {
                return null;
            }
            return new State(prefs);
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(@Nonnull Parcel out, int flags) {
            out.writeInt(width);
            out.writeInt(height);
            out.writeInt(x);
            out.writeInt(y);
        }

        @Override
        public String toString() {
            return "State{" +
                    "y=" + y +
                    ", x=" + x +
                    ", height=" + height +
                    ", width=" + width +
                    '}';
        }

        public void save(@NonNull SharedPreferences.Editor editor) {
            editor.putInt("width", width);
            editor.putInt("height", height);
            editor.putInt("x", x);
            editor.putInt("y", y);
        }
    }
    @NonNull
    private final Context context;
    @NonNull
    private final FloatingViewListener listener;
    @Inject
    Keyboard keyboard;
    @Inject
    Editor editor;
    @Inject
    SharedPreferences preferences;
    @Inject
    Typeface typeface;
    @Named(AppModule.PREFS_FLOATING)
    @Inject
    SharedPreferences myPreferences;
    private View root;
    private View content;
    private View header;
    private ImageView headerTitle;
    private Drawable headerTitleDrawable;
    private EditorView editorView;
    private DisplayView displayView;
    @Nonnull
    private final State state;
    private boolean minimized;
    private boolean attached;
    private boolean folded;
    private boolean initialized;
    private boolean shown;

    public FloatingCalculatorView(@Nonnull Context context,
            @Nonnull State state,
            @NonNull FloatingViewListener listener) {
        cast(context).getComponent().inject(this);
        this.listener = listener;
        final Preferences.SimpleTheme theme =
                Preferences.Onscreen.theme.getPreferenceNoError(preferences);
        final Preferences.Gui.Theme appTheme =
                Preferences.Gui.theme.getPreferenceNoError(preferences);
        final Preferences.SimpleTheme resolvedTheme = theme.resolveThemeFor(appTheme);
        this.context = new ContextThemeWrapper(context, resolvedTheme.light ? R.style.Cpp_Theme_Light : R.style.Cpp_Theme);
        this.root = View.inflate(this.context, theme.getOnscreenLayout(appTheme), null);
        BaseActivity.fixFonts(this.root, typeface);
        final State persistedState = State.fromPrefs(myPreferences);
        if (persistedState != null) {
            this.state = persistedState;
        } else {
            this.state = state;
        }
    }

    static boolean isOverlayPermissionGranted(@NonNull Context context) {
        try {
            final Context application = context.getApplicationContext();
            final WindowManager wm =
                    (WindowManager) application.getSystemService(Context.WINDOW_SERVICE);
            if (wm == null) {
                return false;
            }
            final View view = new View(application);
            wm.addView(view, makeLayoutParams());
            wm.removeView(view);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    public void updateDisplayState(@Nonnull DisplayState displayState) {
        checkInit();
        displayView.setState(displayState);
    }

    private void checkInit() {
        if (!initialized) {
            throw new IllegalStateException("init() must be called!");
        }
    }

    public void updateEditorState(@Nonnull EditorState editorState) {
        checkInit();
        editorView.setState(editorState);
    }

    private void setHeight(int height) {
        checkInit();

        final WindowManager.LayoutParams params =
                (WindowManager.LayoutParams) root.getLayoutParams();
        params.height = height;
        getWindowManager().updateViewLayout(root, params);
    }

    private void init() {
        if (initialized) {
            return;
        }

        for (final CppButton widgetButton : CppButton.values()) {
            final View button = root.findViewById(widgetButton.id);
            if (button == null) {
                continue;
            }
            button.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (keyboard.buttonPressed(widgetButton.action)) {
                        if (keyboard.isVibrateOnKeypress()) {
                            v.performHapticFeedback(KEYBOARD_TAP,
                                    FLAG_IGNORE_GLOBAL_SETTING | FLAG_IGNORE_VIEW_SETTING);
                        }
                    }
                    if (widgetButton == CppButton.app) {
                        minimize();
                    }
                }
            });
            button.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    if (keyboard.buttonPressed(widgetButton.actionLong)) {
                        if (keyboard.isVibrateOnKeypress()) {
                            v.performHapticFeedback(LONG_PRESS,
                                    FLAG_IGNORE_GLOBAL_SETTING | FLAG_IGNORE_VIEW_SETTING);
                        }
                    }
                    return true;
                }
            });
            if (widgetButton == CppButton.erase && button instanceof ImageView) {
                Adjuster.adjustImage((ImageView) button, BaseKeyboardUi.IMAGE_SCALE_ERASE);
            } else {
                BaseKeyboardUi.adjustButton(button);
            }
        }

        final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

        header = root.findViewById(R.id.onscreen_header);
        headerTitle = (ImageView) header.findViewById(R.id.onscreen_title);
        headerTitleDrawable = headerTitle.getDrawable();
        headerTitle.setImageDrawable(null);
        content = root.findViewById(R.id.onscreen_content);

        displayView = (DisplayView) root.findViewById(R.id.calculator_display);

        editorView = (EditorView) root.findViewById(R.id.calculator_editor);
        editorView.setEditor(editor);

        final View onscreenFoldButton = root.findViewById(R.id.onscreen_fold_button);
        onscreenFoldButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (folded) {
                    unfold();
                } else {
                    fold();
                }
            }
        });

        final View onscreenHideButton = root.findViewById(R.id.onscreen_minimize_button);
        onscreenHideButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                minimize();
            }
        });

        root.findViewById(R.id.onscreen_close_button)
                .setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        hide();
                    }
                });

        headerTitle.setOnTouchListener(new MyTouchListener(wm, root));

        initialized = true;

    }

    public void show() {
        if (shown) {
            return;
        }
        init();
        attach();

        shown = true;
    }

    public void attach() {
        checkInit();

        final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        if (!attached) {
            final WindowManager.LayoutParams params = makeLayoutParams();
            params.width = state.width;
            params.height = state.height;
            params.x = state.x;
            params.y = state.y;
            params.gravity = Gravity.TOP | Gravity.LEFT;
            wm.addView(root, params);
            attached = true;
        }
    }

    @Nonnull
    private static WindowManager.LayoutParams makeLayoutParams() {
        return new WindowManager.LayoutParams(
                TYPE_SYSTEM_ALERT,
                FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_WATCH_OUTSIDE_TOUCH,
                PixelFormat.TRANSLUCENT);
    }

    private void fold() {
        if (!folded) {
            headerTitle.setImageDrawable(headerTitleDrawable);
            final Resources r = header.getResources();
            final int newHeight = header.getHeight() + 2 * r
                    .getDimensionPixelSize(R.dimen.cpp_onscreen_main_padding);
            content.setVisibility(View.GONE);
            setHeight(newHeight);
            folded = true;
        }
    }

    private void unfold() {
        if (folded) {
            headerTitle.setImageDrawable(null);
            content.setVisibility(View.VISIBLE);
            setHeight(state.height);
            folded = false;
        }
    }

    public void detach() {
        checkInit();

        if (attached) {
            getWindowManager().removeView(root);
            attached = false;
        }
    }

    public void minimize() {
        checkInit();
        if (!minimized) {
            saveState();
            detach();
            listener.onViewMinimized();
            minimized = true;
        }
    }

    public void hide() {
        checkInit();
        if (!shown) {
            return;
        }
        saveState();
        detach();
        listener.onViewHidden();
        shown = false;
    }

    private void saveState() {
        final SharedPreferences.Editor editor = myPreferences.edit();
        getState().save(editor);
        editor.apply();
    }

    @Nonnull
    private WindowManager getWindowManager() {
        return ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE));
    }

    @Nonnull
    public State getState() {
        final WindowManager.LayoutParams params =
                (WindowManager.LayoutParams) root.getLayoutParams();
        if (!folded) {
            return new State(params.width, params.height, params.x, params.y);
        } else {
            return new State(state.width, state.height, params.x, params.y);
        }
    }
}
