package com.codigoparallevar.minicards;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import com.codigoparallevar.minicards.motion.MotionMode;
import com.codigoparallevar.minicards.parts.buttons.RoundButton;
import com.codigoparallevar.minicards.parts.samples.ColorBox;
import com.codigoparallevar.minicards.parts.samples.Placeholder;
import com.codigoparallevar.minicards.types.InputConnector;
import com.codigoparallevar.minicards.types.OutputConnector;
import com.codigoparallevar.minicards.types.Part;
import com.codigoparallevar.minicards.types.PartGrid;
import com.codigoparallevar.minicards.types.Position;
import com.codigoparallevar.minicards.types.Selectable;
import com.codigoparallevar.minicards.types.Tuple4;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;

class CanvasView extends View implements PartGrid {

    @NonNull
    ArrayList<Part> parts = new ArrayList<>();

    @Nullable
    Selectable selectedPart;

    @Nullable
    final Position lastTouchedPosition = new Position();

    @Nullable
    long lastTouchedTime;

    @Nullable
    private MotionMode.Type motionMode;

    @NonNull
    private String name = "default";
    private final static float touchTimeForLongTouchInMillis = 500;
    private boolean _isDragging = false;
    private MainActivity parentActivity = null;
    private Tuple4<Integer, Integer, Integer, Integer> _dropToRemoveZone = new Tuple4<>(0, 0, 0, 0);
    private boolean _devMode = false;

    public CanvasView(Context context) {
        super(context);
        init();
    }

    public CanvasView(Context context, AttributeSet attr){
        super(context, attr);
        init();
    }

    private void init() {
        this.setBackgroundColor(Color.rgb(4, 69, 99));

        if (!loadState()){
            parts.add(new Placeholder(this, 50, 50, 750, 500));
            parts.add(new ColorBox(this, 250, 250, 100, 100));
            parts.add(new RoundButton(this, 500, 1200, 80, 100));
        }
    }

    private boolean loadState(){
        File filesDir = getContext().getFilesDir();
        File file = new File(filesDir + "/" + name);
        FileReader fileIn = null;
        try {
            fileIn = new FileReader(file);
            char[] data = new char[(int) file.length()];
            fileIn.read(data);
            JSONArray jsonParts = new JSONArray(new String(data ));
            for (int i = 0; i < jsonParts.length(); i++){
                deserializeObject(jsonParts.getJSONObject(i));
            }
        } catch (IOException e) {
            parts.clear();
            Log.w("PartCanvasView", e.getMessage());
            return false;
        } catch (JSONException e) {
            parts.clear();
            Log.w("PartCanvasView", e.getMessage());
            return false;
        }

        try {
            fileIn.close();
        } catch (IOException e) {
            Log.w("PartCanvasView", e.getMessage());
            return false;
        }
        return true;
    }

    private void deserializeObject(JSONObject jsonObject) throws JSONException {
        String type = jsonObject.getString("_type");
        if(type.equals(RoundButton.class.getName())) {
            parts.add(RoundButton.deserialize(this, jsonObject.getJSONObject("_data")));
        }
        else if (type.equals(Placeholder.class.getName())) {
            parts.add(Placeholder.deserialize(this, jsonObject.getJSONObject("_data")));
        }
        else if (type.equals(ColorBox.class.getName())){
            parts.add(ColorBox.deserialize(this, jsonObject.getJSONObject("_data")));
        }
        else {
            throw new JSONException("Expected known class, found " + type);
        }
    }

    @Override
    public void onDraw(Canvas canvas){
        final long renderStartTime = System.currentTimeMillis();
        drawBackground(canvas);

        for (Part part : parts){
            part.draw(canvas, _devMode);
        }

        Log.d("Render time", System.currentTimeMillis() - renderStartTime + "ms");
    }

    private void drawBackground(Canvas canvas) {
        if (!_devMode){
            return;
        }

        // Blueprint background
        final int width = getWidth() + getLeft();
        final int height = getHeight() + getTop();
        final int cellSize = 50;

        Paint blueprintLines = new Paint(Paint.ANTI_ALIAS_FLAG);
        blueprintLines.setColor(Color.argb(100, 255, 255, 255));

        // Vertical lines
        for (int x = cellSize; x < width; x += cellSize){
            canvas.drawLine(x, 0, x, height, blueprintLines);
        }

        // Horizontal lines
        for (int y = cellSize; y < height; y += cellSize){
            canvas.drawLine(0, y, width, y, blueprintLines);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event){
        final int x = (int) event.getX() - this.getLeft();
        final int y = (int) event.getY() - this.getTop();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
            {
                selectedPart = getPartOn(x, y);
                lastTouchedPosition.to(x, y);
                lastTouchedTime = System.currentTimeMillis();
                if (selectedPart == null) {
                    Log.d("Touched part", "not found");
                    return false;
                }

                Log.d("Touched part", "Part: " + selectedPart);
            }
            break;

            case MotionEvent.ACTION_UP:
            {
                if (motionMode == null){
                    motionMode = getMotionMode(false);
                }
                if (motionMode != MotionMode.Type.LongTouch) {
                    if (selectedPart != null){
                        if (selectedPart instanceof Part){
                            ((Part) selectedPart).touched();
                        }
                    }
                }
                else if (motionMode == MotionMode.Type.LongTouch) {
                    if (selectedPart != null) {
                        if (inDropZone(x, y)) {
                            Log.d("Canvas", "Deleting element" + selectedPart);
                            parts.remove(selectedPart);
                        }
                        else {
                            selectedPart.getMoveable().drop(x, y);
                        }
                    }
                }

                _isDragging = false;
                motionMode = null;
                selectedPart = null;
            }
            try {
                saveState();
            } catch (IOException e) {
                Log.w("PartCanvasView", e.getMessage());
            }
            break;

            case MotionEvent.ACTION_MOVE:
            {
                if (!_devMode) {
                    break;
                }

                Log.i("Canvas", "X: " + x + " Y: " + y
                        + " in drop zone " + _dropToRemoveZone + " : " + inDropZone(x, y));
                if (motionMode == null){
                    final Selectable nowSelectedPart = getPartOn(x, y);
                    final boolean canWait = selectedPart == nowSelectedPart;
                    motionMode = getMotionMode(canWait);
                }
                if (motionMode == MotionMode.Type.LongTouch){
                    if (selectedPart != null){
                        _isDragging = true;
                        selectedPart.getMoveable().moveEnd(x, y);
                        invalidate();
                    }
                }
            }
            break;

            default:
            {
                Log.d("PartCanvasView", "Unhandled action: " + event.getAction());
            }
        }

        invalidate();
        if (parentActivity != null) {
            parentActivity.invalidate();
        }
        return true;
    }

    private boolean inDropZone(int x, int y) {
        return (x >= _dropToRemoveZone._x1) && (x <= _dropToRemoveZone._x2)
                && (y >= _dropToRemoveZone._y1) && (y <= _dropToRemoveZone._y2);
    }

    private void saveState() throws IOException {
        File filesDir = getContext().getFilesDir();
        FileOutputStream fileOut = new FileOutputStream(filesDir + "/" + name);

        byte[] serialized = serializeState();
        fileOut.write(serialized);

        fileOut.close();
    }

    private byte[] serializeState() throws IOException {
        JSONArray partArray = new JSONArray();
        for (Part part : parts) {
            JSONObject serializedPart = new JSONObject();
            try {
                serializedPart.put("_type", part.getClass().getName());
                serializedPart.put("_data", part.serialize());
            } catch (JSONException e) {
                throw new IOException(e.getMessage(), e.getCause());
            }

            partArray.put(serializedPart);
        }

        return partArray.toString().getBytes("UTF-8");
    }

    @Nullable
    private MotionMode.Type getMotionMode(boolean canWait) {
        if (selectedPart == null){
            return MotionMode.Type.Displacement;
        }

        if ((System.currentTimeMillis() - lastTouchedTime) >= touchTimeForLongTouchInMillis){
            return MotionMode.Type.LongTouch;
        }

        if (canWait) {
            return null;
        }

        return MotionMode.Type.ShortTouch;
    }

    @Nullable
    public Selectable getPartOn(int x, int y) {
        // Look in the list of parts, in reverse so top-most elements are checked before
        for (int i = parts.size() - 1; i >= 0; i--){
            final Part part = parts.get(i);
            if (part.containsPoint(x, y)){
                return part;
            }
        }

        // If no part was found, do the same for connectors
        for (int i = parts.size() - 1; i >= 0; i--){
            final Part part = parts.get(i);
            // First try with output connectors
            for (OutputConnector outputConnector :  part.getOutputConnectors()){
                if (outputConnector.containsPoint(x, y)){
                    return outputConnector;
                }
            }
            // Then with input ones
            for (InputConnector inputConnector :  part.getInputConnectors()){
                if (inputConnector.containsPoint(x, y)){
                    return inputConnector;
                }
            }
        }

        return null;
    }

    @Override
    @Nullable
    public InputConnector getInputConnectorOn(int x, int y) {
        // If no part was found, do the same for connectors
        for (int i = parts.size() - 1; i >= 0; i--){
            final Part part = parts.get(i);

            // Then with input ones
            for (InputConnector inputConnector :  part.getInputConnectors()){
                if (inputConnector.containsPoint(x, y)){
                    return inputConnector;
                }
            }
        }

        return null;
    }

    public void addPart(Part part) {
        parts.add(part);
        invalidate();
    }

    public boolean isDragging() {
        return _isDragging;
    }

    public void setParentActivity(MainActivity parentActivity) {
        this.parentActivity = parentActivity;
    }

    public void setDropZone(float x1, float x2, float y1, float y2) {
        _dropToRemoveZone = new Tuple4<>((int) x1, (int) x2, (int) y1, (int) y2);
    }

    public void setDevMode(boolean devMode) {
        _devMode = devMode;
        this.invalidate();
    }
}