/*
 * Semitone - tuner, metronome, and piano for Android
 * Copyright (C) 2019  Andy Tockman <[email protected]>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package mn.tck.semitone;

import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;

import java.util.HashMap;

public class PianoView extends View {

    public int rows, keys, pitch;
    protected int whiteWidth, whiteHeight, blackWidth, blackHeight;
    protected Paint whitePaint, grey1Paint, grey3Paint, grey4Paint, blackPaint;

    protected final int OUTLINE = 2, YPAD = 20;
    final int SAMPLE_RATE = 44100;
    final int MAX_TRACKS = 10;

    protected int[][] pitches;
    protected boolean[] pressed;
    HashMap<Integer, Integer> pointers;

    protected int concert_a;
    protected boolean sustain, labelnotes, labelc;

    public PianoView(Context context, AttributeSet attrs) {
        super(context, attrs);

        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
        rows = sp.getInt("piano_rows", 2);
        keys = sp.getInt("piano_keys", 7);
        pitch = sp.getInt("piano_pitch", 28);

        updateParams(false);

        whitePaint = new Paint();
        whitePaint.setColor(ContextCompat.getColor(getContext(), R.color.white));
        grey1Paint = new Paint();
        grey1Paint.setColor(ContextCompat.getColor(getContext(), R.color.grey1));
        grey3Paint = new Paint();
        grey3Paint.setColor(ContextCompat.getColor(getContext(), R.color.grey3));
        grey4Paint = new Paint();
        grey4Paint.setColor(ContextCompat.getColor(getContext(), R.color.grey4));
        blackPaint = new Paint();
        blackPaint.setColor(ContextCompat.getColor(getContext(), R.color.black));
        blackPaint.setTextAlign(Paint.Align.CENTER);

        pressed = new boolean[300];
        pointers = new HashMap<Integer, Integer>();
    }

    public void updateParams(boolean inval) {
        pitches = new int[rows][keys];

        int p = 0;
        for (int i = 0; i < pitch; ++i) p += hasBlackRight(p) ? 2 : 1;
        for (int row = 0; row < rows; ++row) {
            for (int key = 0; key < keys; ++key) {
                pitches[row][key] = p;
                p += hasBlackRight(p) ? 2 : 1;
            }
        }


        if (inval) {
            SharedPreferences.Editor editor =
                PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
            editor.putInt("piano_rows", rows);
            editor.putInt("piano_keys", keys);
            editor.putInt("piano_pitch", pitch);
            editor.apply();

            invalidate();
        }
    }

    @Override protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth(), height = getHeight();

        whiteWidth = width / keys;
        whiteHeight = height / rows;
        blackWidth = whiteWidth * 2 / 3;
        blackHeight = whiteHeight / 2;
        blackPaint.setTextSize(Util.maxTextSize("G0", whiteWidth * 2/3));

        for (int row = 0; row < rows; ++row) {
            for (int key = 0; key < keys; ++key) {
                int x = whiteWidth * key, y = whiteHeight * row;
                int p = pitches[row][key];

                canvas.drawRect(x, y, x + whiteWidth, y + whiteHeight - YPAD, grey3Paint);
                canvas.drawRect(x + OUTLINE, y, x + whiteWidth - OUTLINE,
                        y + whiteHeight - OUTLINE*2 - YPAD,
                        pressed[p] ? grey4Paint : whitePaint);

                if (labelnotes && (!labelc || p % 12 == 0)) canvas.drawText(
                        Util.notenames[(p+3)%12] + (p/12 - 1),
                        x + whiteWidth/2, y + whiteHeight*4/5, blackPaint);

                if (hasBlackLeft(p)) canvas.drawRect(
                        x, y,
                        x + blackWidth / 2, y + blackHeight,
                        pressed[p-1] ? grey1Paint : blackPaint);
                if (hasBlackRight(p)) canvas.drawRect(
                        x + whiteWidth - blackWidth / 2, y,
                        x + whiteWidth, y + blackHeight,
                        pressed[p+1] ? grey1Paint : blackPaint);

            }
        }
    }

    @Override public boolean onTouchEvent(MotionEvent ev) {
        // we want to be able to swipe on the piano without swiping to a
        // different tab
        getParent().requestDisallowInterceptTouchEvent(true);

        int np = ev.getPointerCount();
        int pid, p;

        switch (ev.getActionMasked()) {

        case MotionEvent.ACTION_DOWN:
            pid = ev.getPointerId(0); p = getPitch(ev, 0);
            pointers.put(pid, p);
            play(p);
            invalidate();
            return true;

        case MotionEvent.ACTION_POINTER_DOWN:
            pid = ev.getPointerId(ev.getActionIndex()); p = getPitch(ev, ev.getActionIndex());
            pointers.put(pid, p);
            play(p);
            invalidate();
            return true;

        case MotionEvent.ACTION_MOVE:
            boolean anyChange = false;
            for (int i = 0; i < np; ++i) {
                pid = ev.getPointerId(i); p = getPitch(ev, i);
                if (pointers.get(pid) != p) {
                    stop(pointers.get(pid));
                    pointers.put(pid, p);
                    play(p);
                    anyChange = true;
                }
            }
            if (anyChange) invalidate();
            return true;

        case MotionEvent.ACTION_UP:
            stop(pointers.remove(ev.getPointerId(0)));
            invalidate();
            return true;

        case MotionEvent.ACTION_POINTER_UP:
            stop(pointers.remove(ev.getPointerId(ev.getActionIndex())));
            invalidate();
            return true;

        }

        return false;
    }

    private int getPitch(MotionEvent ev, int pidx) {
        int row = Math.min((int)(ev.getY(pidx) / whiteHeight), rows-1),
            key = Math.min((int)(ev.getX(pidx) / whiteWidth), keys-1),
            p = pitches[row][key];

        if (ev.getY(pidx) - row*whiteHeight < blackHeight) {
            // we're high enough to hit a black key - check if we do
            int x = (int)(ev.getX(pidx) - key*whiteWidth);
            if (x < blackWidth/2 && hasBlackLeft(p)) --p;
            else if (x > whiteWidth - blackWidth/2 && hasBlackRight(p)) ++p;
        }

        return p < 0 || p > 128 ? 0 : p;
    }

    private void play(int pitch) {
        pressed[pitch] = true;
        if (sustain) PianoEngine.play(pitch, concert_a);
        else PianoEngine.playFile("piano/"+pitch+".mp3", concert_a);
    }

    private void stop(int pitch) {
        pressed[pitch] = false;
        if (sustain) PianoEngine.stop(pitch);
    }

    protected boolean hasBlackLeft(int p) { return p % 12 != 5 && p % 12 != 0; }
    protected boolean hasBlackRight(int p) { return p % 12 != 4 && p % 12 != 11; }

}