using UnityEngine;
using System;
using System.Collections;

abstract class Note
{
    // define common note properties

    public double Time;
    public HandType HandType;

    protected Level level;
    protected NoteObject noteObject;

    private bool isVisible = true;
    private bool isActive = true; 

    public bool IsActive { get => isActive; } // judegement is active (not hit yet)
    public bool IsVisible { get => isVisible; } // note is visible


    // interpret note option
    protected abstract void FromBmsNum(String num);
    protected abstract NoteObject CreateNoteObjectImpl();

    public void Activate()
    {
        isActive = true;
    }

    public void Deactivate()
    {
        isActive = false;
    }


    // instantiate associated game object
    public void CreateNoteObject()
    {
        noteObject = CreateNoteObjectImpl();
    }

    // override this function to implement per-note judgement.
    public virtual JudgeType JudgeTiming(double time)
    {
        double delta = time - this.Time;
        var timing = Math.Abs(delta);
        var early = delta < 0;

        if (timing < 0.1)
        {
            return JudgeType.Perfect;
        }
        else if (timing < 0.5)
        {
            return JudgeType.Hit;
        } else if (timing < 1.0 && early)
        {
            return JudgeType.Miss;
        }
        else
        {
            return JudgeType.Ignore;
        }
    }

    public virtual HitType CheckHit(PlayerInput input)
    {
        // add near support

        if(noteObject.IsHit(input))
        {
            return HitType.Hit;
        } else
        {
            return HitType.Miss;
        }
    }

    public void HandleJudge(JudgeResult judge)
    {
        if (judge.type != JudgeType.Ignore)
        {
            Debug.Log(string.Format("Note #{0}: {1}, Correct Hand: {2}", 
                0,
                System.Enum.GetName(typeof(JudgeType), judge.type),
                judge.correctHand
            ));
            noteObject.NoteHit(judge);
        }
    }


    // TODO: Refactor this
    private static readonly float NOTE_SHOW_TIMING = 5;
    private static readonly float NOTE_HIDE_TIMING = -2;
    private static readonly float NOTE_MISS_TIMING = -0.5f;
    private static readonly float NOTE_POSITION_MULTIPLIER = 2;
    public void Update(float remainingTime)
    {
        if (remainingTime < NOTE_HIDE_TIMING * NOTE_POSITION_MULTIPLIER || remainingTime > NOTE_SHOW_TIMING)
        {
            noteObject.gameObject.SetActive(false);
            isVisible = false;
        }
        else
        {
            noteObject.gameObject.SetActive(true);
            isVisible = true;
        }

        if (remainingTime < NOTE_MISS_TIMING && isActive && isVisible)
        {
            HandleJudge(new JudgeResult(JudgeType.Miss));
            Deactivate();
        }

        noteObject.SetPosition(remainingTime/NOTE_SHOW_TIMING * NOTE_POSITION_MULTIPLIER);
    }

    // factory method
    // may return null if given note is not supported
    public static Note Create(BmsNote bn, double bpm, Level parent)
    {
        Note note = null;
        HandType handType = HandType.None;

        switch (bn.lane)
        {
            case "11": // 1p first lane: FORWARD LEFT HAND
                note = new ForwardNote();
                handType = HandType.Left;
                break;
            case "12": // 1p second lane: FORWARD RIGHT HAND
                note = new ForwardNote();
                handType = HandType.Right;
                break;
            case "13": // 1p third lane: EDGE LEFT HAND
                note = new EdgeNote();
                handType = HandType.Left;

                break;
            case "14": // 1p forth lane: EDGE RIGHT HAND
                note = new EdgeNote();
                handType = HandType.Right;

                break;

            case "16": // 1p forth lane: EDGE RIGHT HAND
                note = new ReloadNote();
                handType = HandType.None;

                break;

            default:
                return null;
        }

        note.FromBmsNum(bn.num);
        note.HandType = handType;
        note.Time = bn.barTime * (60.0 / bpm * 4);

        note.level = parent;

        note.CreateNoteObject();

        return note;
    }
}

class ForwardNote : Note
{
    private float x, y;

    protected override NoteObject CreateNoteObjectImpl()
    {
        var obj = MonoBehaviour.Instantiate<ForwardNoteObject>(PlayEngine.inst.ForwardNoteObject);
        obj.Init(x, y, HandType);
        return obj;
    }

    protected override void FromBmsNum(string num)
    {
        //Debug.Log(num);
        // since 00 is used for empty notes, 1 ~ Z is valid range
        x = charToCoord(num[0]) / 9.0f;
        y = charToCoord(num[1]) / 9.0f;
    }

    private int charToCoord(char a)
    {
        if (char.IsDigit(a))
        {
            return a - '1';
        }
        else if (char.IsLetter(a))
        {
            return char.ToLower(a) - 'a' + 9;
        }

        throw new ArgumentException("wrong data");
    }


}

class RearNote : Note
{
    protected override NoteObject CreateNoteObjectImpl()
    {
        throw new NotImplementedException();
    }

    protected override void FromBmsNum(string num)
    {
        throw new NotImplementedException();
    }
}

class EdgeNote : Note
{
    private int index;

    protected override NoteObject CreateNoteObjectImpl()
    {
        var obj = MonoBehaviour.Instantiate<EdgeNoteObject>(PlayEngine.inst.EdgeNoteObjects[index]);
        obj.Init(HandType);
        return obj;
    }

    protected override void FromBmsNum(string num)
    {
        index = charToIndex(num);
    }

    private int charToIndex(string hex)
    {
        if (hex[0] != 'E')
        {
            throw new ArgumentException("wrong data");
        }
        if (char.IsDigit(hex[1]))
        {
            return hex[1] - '0';
        }

        throw new ArgumentException("wrong data");
    }

    private int GetPrefabIndex()
    {
        return 1;
    }
}

abstract class ActionNote : Note
{
    public abstract JudgeType CheckMotion(PlayerMotion motion);
}

class ReloadNote : ActionNote
{
    private int index;

    public override JudgeType CheckMotion(PlayerMotion motion)
    {
        if (CheckHand(motion.rightHand) && CheckHand(motion.leftHand))
        {
            return JudgeType.Hit;
        }

        return JudgeType.Miss;
    }

    private bool CheckHand(MotionTracker hand)
    {
        return Vector3.Dot(hand.Velocity, Vector3.down) > 0.002;
    }

    protected override NoteObject CreateNoteObjectImpl()
    {
        var obj = MonoBehaviour.Instantiate<ReloadNoteObject>(PlayEngine.inst.ReloadNoteObject);
        obj.Init();
        return obj;
    }


    protected override void FromBmsNum(string num)
    {

    }
}


public enum NoteType
{
    Front,
    Edge,
    Reload
}

public enum HandType
{
    Left,
    Right,
    None
}