/* date = May 7th 2023 7:16 pm */

#ifndef VN_TEXT_OP_H
#define VN_TEXT_OP_H

struct text_op
{
    range1_s64 Range;
    string ReplaceString;
    string CopyString;
    s64 NewCursor;
    s64 NewMark;
};

struct text_edit_state
{
    s64 Cursor;
    s64 Mark;
};

typedef u32 text_action_flags;
enum
{
    TextActionFlag_WordScan                = (1<<0),
    TextActionFlag_KeepMark                = (1<<1),
    TextActionFlag_Delete                  = (1<<2),
    TextActionFlag_ZeroDeltaWithSelection  = (1<<3),
    TextActionFlag_DeltaPicksSelectionSide = (1<<4),
    TextActionFlag_Copy                    = (1<<5),
    TextActionFlag_Paste                   = (1<<6),
    TextActionFlag_OperateOnLine           = (1<<7),
    TextActionFlag_StopOnNewline           = (1<<8),
    
    TextActionFlag_EmptyLineScan           = TextActionFlag_WordScan,
};

struct text_action
{
    text_action_flags Flags;
    s64 Delta;
    u32 Codepoint;
};

inline b32 IsValid(text_action *Action)
{
    b32 Result = !(Action->Flags == 0 && Action->Delta == 0 && Action->Codepoint == 0);
    return(Result);
}

static text_action SingleLineTextActionFromEvent(platform_event *Event)
{
    text_action Action = {};
    
    if(Event->Type == PlatformEvent_Text)
    {
        if(Event->Codepoint != '\n')
        {
            Action.Codepoint = Event->Codepoint;
        }
    }
    else if(Event->Type == PlatformEvent_Press)
    {
        if(Event->Modifiers & PlatformModifier_Ctrl)
        {
            Action.Flags |= TextActionFlag_WordScan;
        }
        if(Event->Modifiers & PlatformModifier_Shift)
        {
            Action.Flags |= TextActionFlag_KeepMark;
        }
        
        switch(Event->Key)
        {
            case Key_Right:
            {
                Action.Delta = +1;
                Action.Flags |= TextActionFlag_DeltaPicksSelectionSide;
            } break;
            
            case Key_Left:
            {
                Action.Delta = -1;
                Action.Flags |= TextActionFlag_DeltaPicksSelectionSide;
            } break;
            
            case Key_Home:  { Action.Delta = S64_Min; } break;
            case Key_End:   { Action.Delta = S64_Max; } break;
            
            case Key_Backspace:
            {
                Action.Delta = -1;
                Action.Flags |= TextActionFlag_Delete|TextActionFlag_ZeroDeltaWithSelection;
            } break;
            
            case Key_Delete:
            {
                Action.Delta = +1;
                Action.Flags |= TextActionFlag_Delete|TextActionFlag_ZeroDeltaWithSelection;
            } break;
            
            case Key_C: if(Event->Modifiers & PlatformModifier_Ctrl)
            {
                Action.Flags |= TextActionFlag_Copy;
            } break;
            
            case Key_V: if(Event->Modifiers & PlatformModifier_Ctrl)
            {
                Action.Flags |= TextActionFlag_Paste;
            } break;
            
            case Key_X: if(Event->Modifiers & PlatformModifier_Ctrl)
            {
                Action.Flags |= TextActionFlag_Copy|TextActionFlag_Delete;
            } break;
            
            default: {} break;
        }
    }
    return(Action);
}

static text_action MultiLineTextActionFromEvent(platform_event *Event)
{
    text_action Action = {};
    
    if(Event->Type == PlatformEvent_Text)
    {
        Action.Codepoint = Event->Codepoint;
    }
    else if(Event->Type == PlatformEvent_Press)
    {
        if(Event->Modifiers & PlatformModifier_Ctrl)
        {
            Action.Flags |= TextActionFlag_WordScan;
        }
        if(Event->Modifiers & PlatformModifier_Shift)
        {
            Action.Flags |= TextActionFlag_KeepMark;
        }
        
        switch(Event->Key)
        {
            case Key_Right:
            {
                Action.Delta = +1;
                Action.Flags |= TextActionFlag_DeltaPicksSelectionSide;
            } break;
            
            case Key_Left:
            {
                Action.Delta = -1;
                Action.Flags |= TextActionFlag_DeltaPicksSelectionSide;
            } break;
            
            case Key_Down:
            {
                Action.Delta = +1;
                Action.Flags |= TextActionFlag_OperateOnLine;
            } break;
            
            case Key_Up:
            {
                Action.Delta = -1;
                Action.Flags |= TextActionFlag_OperateOnLine;
            } break;
            
            case Key_Home:
            {
                Action.Delta = S64_Min;
                Action.Flags |= TextActionFlag_StopOnNewline;
            } break;
            
            case Key_End:
            {
                Action.Delta = S64_Max;
                Action.Flags |= TextActionFlag_StopOnNewline;
            } break;
            
            case Key_Backspace:
            {
                Action.Delta = -1;
                Action.Flags |= TextActionFlag_Delete|TextActionFlag_ZeroDeltaWithSelection;
            } break;
            
            case Key_Delete:
            {
                Action.Delta = +1;
                Action.Flags |= TextActionFlag_Delete|TextActionFlag_ZeroDeltaWithSelection;
            } break;
            
            case Key_C: if(Event->Modifiers & PlatformModifier_Ctrl)
            {
                Action.Flags |= TextActionFlag_Copy;
            } break;
            
            case Key_V: if(Event->Modifiers & PlatformModifier_Ctrl)
            {
                Action.Flags |= TextActionFlag_Paste;
            } break;
            
            case Key_X: if(Event->Modifiers & PlatformModifier_Ctrl)
            {
                Action.Flags |= TextActionFlag_Copy|TextActionFlag_Delete;
            } break;
            
            default: {} break;
        }
    }
    return(Action);
}

inline s64 CodepointScan(string String, s64 Index, s64 Delta, b32 StopOnNewline)
{
    s64 Result = 0;
    if(Delta > 0)
    {
        while(Index < String.Count && Delta)
        {
            u8 Base = String.Data[Index];
            s64 ToMove = 0;
            
            if(StopOnNewline && Base == '\n')
            {
                break;
            }
            
            if((Base & 0x80) == 0x00)
            {
                ToMove = 1;
            }
            else if((Base & 0xE0) == 0xC0)
            {
                ToMove = 2;
            }
            else if((Base & 0xF0) == 0xE0)
            {
                ToMove = 3;
            }
            else if((Base & 0xF8) == 0xF0)
            {
                ToMove = 4;
            }
            
            Result += ToMove;
            Index += ToMove;
            
            --Delta;
        }
    }
    else
    {
        Index -= 1;
        
        while(Index >= 0 && (Delta != 0))
        {
            u8 Base = String.Data[Index];
            
            if(StopOnNewline && Base == '\n')
            {
                break;
            }
            
            
            if(((Base & 0x80) == 0) || !((Base & 0xC0) == 0x80))
            {
                ++Delta;
            }
            
            --Result;
            --Index;
        }
    }
    
    return(Result);
}

inline b32 IsWordBoundary(string String, s64 Index)
{
    b32 Result;
    if(Index > 0)
    {
        Result = IsWhitespace(String.Data[Index - 1]) && !(IsWhitespace(String.Data[Index]));
    }
    else
    {
        Result = true;
    }
    return(Result);
}

static s64 WordScan(string String, s64 Index, s64 Delta)
{
    s64 Result = 0;
    
    while(Delta)
    {
        if(Delta > 0)
        {
            ++Index;
            ++Result;
            
            while(Index < String.Count && !IsWordBoundary(String, Index))
            {
                ++Index;
                ++Result;
            }
            
            if(Index > String.Count)
            {
                Result -= Index - String.Count;
                Index = String.Count;
                goto End;
            }
            
            --Delta;
        }
        else
        {
            --Index;
            --Result;
            
            while(Index >= 0 && !IsWordBoundary(String, Index))
            {
                --Index;
                --Result;
            }
            
            if(Index < 0)
            {
                Result -= Index;
                Index = 0;
                goto End;
            }
            
            ++Delta;
        }
    }
    
    End:
    return(Result);
}

static b32 StringIsWhitespace(string String)
{
    b32 Result = true;
    
    u8 *StringBegin = String.Data;
    u8 *StringEnd = StringBegin+String.Count;
    
    for(u8 *Char = StringBegin; Char < StringEnd; Char += 1)
    {
        if(!IsWhitespace(*Char) && *Char != '\0')
        {
            Result = false;
            break;
        }
    }
    
    return(Result);
}

static text_op TextOpFromAction(memory_arena *Arena, string String,
                                text_edit_state *State, text_action *Action,
                                range1_s64_array *Lines = 0, s64 LastColumnIndex = 0)
{
    text_op Op = {};
    
    Op.NewCursor = State->Cursor;
    Op.NewMark = State->Mark;
    Op.Range = Range1S64(0, 0);
    Op.ReplaceString = StrLit("");
    
    //- sixten: navtigation
    s64 Delta = 0;
    if(Action->Flags & TextActionFlag_OperateOnLine)
    {
        //- sixten: determine what line we are on.
        s64 LineIndex = 0;
        s64 ColumnIndex = 0;
        {
            u8 *TextBegin = String.Data;
            u8 *TextEnd = TextBegin + State->Cursor;
            u8 *Char = TextBegin;
            for(;Char < TextEnd; Char += 1)
            {
                ColumnIndex += 1;
                if(*Char == '\n')
                {
                    LineIndex += 1;
                    ColumnIndex = 0;
                }
            }
        }
        
        s64 LineDelta = Action->Delta;
        
        if(Action->Flags & TextActionFlag_EmptyLineScan)
        {
            // sixten: if the line we start on is blank, we want to make sure that there has been a text containing non-whitespace before stopping.
            b32 IgnoreFirstWhitespace = StringIsWhitespace(Substring(String, Lines->Ranges[LineIndex]));
            
            for(;0 <= LineIndex && LineIndex < Lines->Count; LineIndex += Action->Delta)
            {
                string Line = Substring(String, Lines->Ranges[LineIndex+Action->Delta]);
                if(StringIsWhitespace(Line))
                {
                    if(!IgnoreFirstWhitespace)
                    {
                        break;
                    }
                }
                else
                {
                    IgnoreFirstWhitespace = false;
                }
                
#if 0                
                u8 *LineBegin = Line.Data;
                u8 *LineEnd = LineBegin+Line.Count;
                u8 *Char = (Action->Delta > 0)?LineBegin:LineEnd;
                
                if(Action->Delta > 0)
                {
                    for(;Char <= LineEnd; Char += 1)
                    {
                        if(Char == LineEnd)
                        {
                            goto FoundLine;
                        }
                        if(!IsWhitespace(*Char))
                        {
                            break;
                        }
                    }
                }
                else if(Action->Delta < 0)
                {
                    for(;LineBegin <= Char; Char -= 1)
                    {
                        if(!IsWhitespace(*Char))
                        {
                            break;
                        }
                        
                        if(Char == LineBegin)
                        {
                            goto FoundLine;
                        }
                    }
                }
#endif
            }
        }
        
        u64 ColumnOffset = Max(LastColumnIndex - ColumnIndex, 0LLU);
        
        //- sixten: check that the line we are trying to access is inbounds, else just go to the start or end of the text.
        if(InRange(Range1S64(0, Lines->Count), LineIndex + LineDelta))
        {
            Delta = Lines->Ranges[LineIndex+LineDelta].Min - Lines->Ranges[LineIndex].Min + ColumnOffset;
            if(!InRange(Lines->Ranges[LineIndex+LineDelta], State->Cursor + Delta))
            {
                Delta = Lines->Ranges[LineIndex+LineDelta].Max - State->Cursor - 1;
            }
        }
        else
        {
            s64 TempDelta = (LineDelta > 0)?S64_Max:S64_Min;
            Delta = CodepointScan(String, State->Cursor, TempDelta, Action->Flags & TextActionFlag_StopOnNewline);
        }
    }
    
    else
    {
        if(Action->Flags & TextActionFlag_WordScan)
        {
            Delta = WordScan(String, State->Cursor, Action->Delta);
        }
        else
        {
            Delta = CodepointScan(String, State->Cursor, Action->Delta, Action->Flags & TextActionFlag_StopOnNewline);
        }
    }
    if(State->Cursor != State->Mark &&
       Action->Flags & TextActionFlag_ZeroDeltaWithSelection)
    {
        Delta = 0;
    }
    
    if(State->Cursor != State->Mark &&
       Action->Flags & TextActionFlag_DeltaPicksSelectionSide &&
       !(Action->Flags & TextActionFlag_KeepMark))
    {
        Delta = 0;
        if(Action->Delta > 0)
        {
            Op.NewCursor = Maximum(State->Cursor, State->Mark);
        }
        else if(Action->Delta < 0)
        {
            Op.NewCursor = Minimum(State->Cursor, State->Mark);
        }
    }
    
    Op.NewCursor = State->Cursor + Delta;
    
    //- sixten: post-navigation
    
    if(Action->Flags & TextActionFlag_Copy)
    {
        string CopyString = Substring(String, Range1S64(Op.NewCursor, Op.NewMark));
        Platform.SetClipboard(CopyString);
    }
    
    if(Action->Flags & TextActionFlag_Delete)
    {
        Op.Range = Range1S64(Op.NewCursor, Op.NewMark);
        Op.NewCursor = Op.NewMark = Op.Range.Min;
    }
    
    if(Action->Codepoint != 0)
    {
        Op.ReplaceString = StringFromCodepoint(Arena, Action->Codepoint);
        
        if(State->Cursor == State->Mark)
        {
            Op.NewCursor += Op.ReplaceString.Count;
            Op.Range = Range1S64(State->Cursor, State->Cursor);
        }
        else
        {
            Op.NewCursor += Op.ReplaceString.Count;
            Op.Range = Range1S64(State->Cursor, State->Mark);
        }
    }
    else if(Action->Flags & TextActionFlag_Paste)
    {
        Op.ReplaceString = Platform.GetClipboard(Arena);
        Op.Range = Range1S64(State->Cursor, State->Cursor);
        Op.NewCursor += Op.ReplaceString.Count;
    }
    
    if(!(Action->Flags & TextActionFlag_KeepMark))
    {
        Op.NewMark = Op.NewCursor;
    }
    
    s64 NewStringCount = String.Count-DimOfRange(Op.Range)+Op.ReplaceString.Count;
    Op.NewCursor = Clamp(Op.NewCursor, 0, NewStringCount);
    Op.NewMark = Clamp(Op.NewMark, 0, NewStringCount);
    
    return(Op);
}

#endif //VN_TEXT_OP_H
