//
//  GSOSConsole.m
//  2Term
//
//  Created by Kelvin Sherlock on 7/9/2016.
//
//

#import "GSOSConsole.h"

#include "OutputChannel.h"
#include "Screen.h"

#include "algorithm.h"

// currently no way to set consFill :-)
static char_info remap_fill(const gsos_context &ctx) { return char_info(' ', 0); }

static void advance(Screen *screen, gsos_context &ctx) {

    if (ctx.consAdvance) {
        if (ctx.cursor.x < ctx.window.maxX() - 1) ctx.cursor.x++;
        else if (ctx.consWrap) {
            ctx.cursor.x = ctx.window.minX();
            if (ctx.cursor.y < ctx.window.maxY() - 1) ctx.cursor.y++;
            else if (ctx.consScroll) screen->scrollUp(ctx.window);
        }
    }

}

%%{
    machine console;
    alphtype unsigned int;
    
    action nop {}
    
    arg1 = any ${ _scratch[0] = (fc - 32); };
    arg2 = any ${ _scratch[1] = (fc - 32); };
    arg3 = any ${ _scratch[2] = (fc - 32); };
    arg4 = any ${ _scratch[3] = (fc - 32); };
    
    
    action advance_if {
        if (_context.consAdvance) advance(screen, _context);
    }
    
    main := (
               0x00 $nop
             | 0x01 ${
                 /*
                  Save Current Text Port and Reset Default Text Port
                  
                  Saves the current text port and resets to the default text port. If the system is out of memory, no error is returned, and the text port is simply reset.
                  */
                 
                 _context_stack.push_back(_context);
                 _context = gsos_context();
                 _context.cursor = iPoint(0,0);
                 _context.window = iRect(0, 0, 80, 24);
             }

             | 0x02 arg1 arg2 arg3 arg4 ${
                 /*
                  Set Text Port Size

                  Accepts the next four bytes as absolute screen coordinates + 32. Sets the current text port to the new parameters. The parameters are in the following order: windLeft, windTop, windRight, windBottom. Any parameter outside the screen boundaries is clipped to a legal value. The cursor is set to the upper-left corner of the new text port.
                  */

                 _scratch[0] = clamp(_scratch[0], 0, 80-1);
                 _scratch[1] = clamp(_scratch[1], 0, 24-1);

                 _scratch[2] = clamp(_scratch[2], 0, 80-1)+1;
                 _scratch[3] = clamp(_scratch[3], 0, 24-1)+1;
                 
                 
                 window = iRect(_scratch[0],
                         _scratch[1],
                         _scratch[2] - _scratch[0],
                         _scratch[3] - _scratch[1]
                         );
                cursor = window.origin;
             }
             
             | 0x03 ${
                 /*
                  Clear from Beginning of Line

                  Clears all characters from the left edge to and including the cursor. Sets them to the current consFill character.
                  */
                 
                 char_info ci = remap_fill(_context);
                 iRect tmp(
                    iPoint(window.minX(), cursor.y),
                    iPoint(cursor.x+1, cursor.y+1)
                 );
                 screen->fillRect(tmp, ci);
                 
             }
             
             | 0x04 ${
                 /*
                  Pop Text Port

                  Restores the text port to the most recently saved value (see code $01). If no saved ports exist, resets the text port to the default values. If an 80-column text port is pushed and subsequently restored in 40-column mode, the text port may be altered to fit in the 40-column screen (see code $11, Set 40-Column Mode).
                  */
             
                 if (!_context_stack.empty()) {
                     _context = _context_stack.back();
                     _context_stack.pop_back();
                 } else {
                     _context = gsos_context();
                 }
             
             }

             | 0x05 any ${
                 /* 
                  Horizontal Scroll

                  Interprets the next byte as an 8-bit signed integer depicting the number (N) of
                  columns to shift. N equal to zero is a null operation. If N is less than zero, the text
                  port is shifted to the left; A greater than zero shifts to the right. If the shift magnitude is equal to or greater than windWidth, the text port is cleared.
                  The shifted characters are moved directly to their destination location. The space vacated by the shifted characters is set to the current consFill character (see the description of consFill earlier in this chapter). Characters shifted out of the text port are removed from the screen and are not recoverable.
                  */
                 int8_t n = fc;
                 if (n < 0) {
                     screen->scrollLeft(window, n);
                 }
                 if (n > 0) {
                     screen->scrollRight(window, n);
                 }
                 // .... TODO
             }

             | 0x06 any ${
                 /* 
                  Set Vertical Position

                  Interprets the next byte as a text port-relative vertical position + 32. If the destination is outside the current text port, the cursor is moved to the nearest edge.
                  */
                 unsigned n = clamp(fc - 32, 0, window.height()-1);
                 cursor.y = window.minY() + n;
             }

             | 0x07 ${
                 /* 
                  Ring Bell

                  Causes the System Beep to be played. It has no effect on the screen.
                  */
                 NSBeep();
             }

             | 0x08 ${
                 /* 
                  Backspace
                  
                  Moves the cursor one position to the left. If the cursor was on the left edge of the
                  text port and consWrap is TRUE, the cursor is placed one row higher and at the right edge. If the cursor was also on the topmost row and consScroll is TRUE, thetext port will scroll backward one line.
                  */
                 if (cursor.x > window.minX()) cursor.x--;
                 else if (_context.consWrap) {
                     cursor.x = window.maxX() - 1;
                     if (cursor.y > window.minY()) cursor.y--;
                     else if (_context.consScroll) screen->scrollDown(window);
                 }
                 
             }

             | 0x09 $nop

             | 0x0a ${
                 /* 
                  Line Feed
                  
                  Causes the cursor to move down one line. If the cursor was at the bottom edge of the text port and consScroll is TRUE, the text port scrolls up one line.
                  */
                 if (cursor.y < window.maxY()-1) cursor.y++;
                 else if (_context.consScroll)
                    screen->scrollUp(window);
             }

             | 0x0b ${
                 /* 
                  Clear to End of Text Port
                  
                  Clears all characters from the cursor to the end of the current text port and sets them to be equal to the current consFill character.
                  */

                iRect tmp;
                tmp.origin = cursor;
                tmp.size = iSize(window.size.width - cursor.x, 1);

                screen->eraseRect(tmp);

                tmp = window;
                tmp.origin.y = cursor.y+1;
                tmp.size.height -= cursor.y+1;
                screen->eraseRect(tmp);
             }


             | 0x0c ${
                 /*
                  Clear Text Port and Home Cursor
                  
                  Clears the entire text port and resets the cursor to windLeft, windTop.
                  */

                 screen->eraseRect(window);
                 cursor = window.origin;
             }

             | 0x0d ${
                 /* 
                  Carriage Return
                  
                  Resets the cursor to the left edge of the text port; if consLF is TRUE, performs a line feed (see $0A, Line Feed).
                  */

                 cursor.x = window.minX();
                 if (_context.consLF) {
                     if (cursor.y < window.maxY() - 1) cursor.y++;
                     else if (_context.consScroll) screen->scrollUp(window);
                 }
             }

             | 0x0e ${
                 /* 
                  Set Normal Display Mode
                  
                  After this character, displays all subsequent characters in normal mode,
                  */
                 _context.clearFlagBit(Screen::FlagInverse);
                 _context.consFill = 0xa0;
                 _context.consVideo = true;
             }
             
             | 0x0f ${
                 /*
                  Set Inverse Display Mode
                  After this character, displays all subsequent characters in inverse mode.
                  */
                 _context.setFlagBit(Screen::FlagInverse);
                 _context.consFill = 0x20;
                 _context.consVideo = false;
             }

             | 0x10 any ${
                /*
                 DLE Space Expansion

                 If consDLE is TRUE, interprets the next character as number of spaces + 32, and the
                 correct number of spaces is issued to die screen. If consDLE is FALSE, the DLE character is ignored and the following character is processed normally.
                 */
                if (_context.consDLE) {
                    unsigned count = (fc - 32) & 0xff;
                    //char_info ci = remap_fill(_context);

                    if (_context.consAdvance) {
                        while (count--) {
                            screen->putc(' ', _context);
                            if (_context.consAdvance) advance(screen, _context);
                        }
                    }
                    else {
                        if (count) screen->putc(' ', _context);
                    }
                }
                else { fhold; }
            }

             | 0x11 ${ /* 40 column mode */ }
             | 0x12 ${ /* 80 column mode */ }

             | 0x13 ${

             }

             | 0x14 any ${
                 /*
                  Set Horizontal Position
                  
                  Interprets the next byte as a text port-relative horizontal position + 32. If the destination is outside the current text port, the cursor is moved to the nearest edge.
                  */
                 unsigned n = clamp(fc - 32, 0, window.width() - 1);
                 cursor.x = window.minX() + n;
             }
             
             | 0x15 any ${
                 /* 
                  Set Cursor Movement Word
                  
                  Interprets the next byte as cursor movement control, and sets the values of these Boolean flags:
                  */
                  
                 unsigned flags = fc;
                 _context.consAdvance = flags & 0x01;
                 _context.consLF = flags & 0x02;
                 _context.consWrap = flags & 0x04;
                 _context.consScroll = flags & 0x08;
                 _context.consDLE = flags & 0x10;
             }

             | 0x16 ${
                /*
                 Scroll Down One Line
                 
                 Scrolls the text port down one line. Does not move the cursor.
                 */
                screen->scrollDown(window);
             }

             | 0x16 ${
                /*
                 Scroll Up One Line
                 
                 Scrolls the text port up one line. Does not move the cursor.
                 */
                screen->scrollUp(window);
             }

             | 0x18 ${
                 /* 
                  Disable MouseText Mapping
                  
                  When MouseText is disabled, uppercase inverse characters are displayed as such (see the section "Character Set Mapping" earlier in this chapter).
                  */
                 _context.clearFlagBit(Screen::FlagMouseText);
                 _context.consMouse = false;
             }

             | 0x19 ${
                 /*
                  Home Cursor
                  
                  Resets the cursor to the upper-left corner of the text port.
                  */
                 cursor = window.origin;
             }

             | 0x1a ${
                 /*
                  Clear Line
                  
                  Clears the line that the cursor is on. Resets the cursor to the leftmost column in the window.
                  */
                 iRect tmp;
                 tmp.origin = iPoint(window.origin.x, cursor.y);
                 tmp.size = iSize(window.size.width, 1);
                 screen->eraseRect(tmp);
             }

             | 0x1b ${
                 /*
                  Enable MouseText Mapping
                  
                  When MouseText is enabled, uppercase inverse letters are displayed as MouseText symbols (see the section "Character Set Mapping" earlier in this chapter).
                  */
                 _context.setFlagBit(Screen::FlagMouseText);
                 _context.consMouse = true;
             }

             | 0x1c ${
                 /*
                  Move Cursor Right
                  
                  Performs a nondestructive forward-space of the cursor. If consWrap is TRUE,
                  the cursor may go to the next line; if consScroll is TRUE, the screen may scroll up one line.
                  */
                 advance(screen, _context);
             }

             | 0x1d ${
                 /*
                  Clear to End of Line
                  
                  Clears from the position underneath the cursor to the end of the current line.
                  */
                 iRect tmp;
                 tmp.origin = cursor;
                 tmp.size = iSize(window.size.width - cursor.x, 1);
                 
                 screen->eraseRect(tmp);

             }

             | 0x1e arg1 arg2 ${
                    /* 
                     Go to X,Y
                     
                     Adjusts the cursor position relative to the text port. The parameters passed are X+32
                     and Y+32. If the new locations are outside the current text port, the cursor is placed on the nearest edge.
                     */
                    
                    iPoint dca;

                    dca.x = clamp(_scratch[0], 0, window.width() - 1);
                    dca.y = clamp(_scratch[1], 0, window.height() - 1);
                    cursor.x = dca.x + window.minX();
                    cursor.y = dca.y + window.minY();
                    cursor = dca;
                }

             | 0x1f ${
                 /* 
                  Move Cursor Up
                  
                  Moves the cursor up one line (reverse line feed). If the cursor is already on the
                  uppermost line of the textport and consScroll isTRUE, it will cause a reverse scroll.
                  */
                 if (cursor.y > window.minY()) cursor.y--;
                 else if (_context.consScroll) screen->scrollDown(window);
             }

             | 0x20 .. 0x7f ${
                 screen->putc(fc, _context);
             } $advance_if

             | 0x80 .. 0x9f ${
                 /* uppercase inverse/normal */
                 uint8_t flags = _context.flags ^ Screen::FlagInverse;
                 screen->putc(fc - 0x40, cursor, flags);
             } $advance_if

             | 0xa0 .. 0xbf ${
                 /* special inverse/normal */
                 uint8_t flags = _context.flags ^ Screen::FlagInverse;
                 screen->putc(fc - 0x80, cursor, flags);
             } $advance_if

             | 0xc0 .. 0xdf ${
                 /* uppercase normal / mouse text. */
                 uint8_t flags = _context.flags ^ Screen::FlagInverse;
                 if (flags) flags |= Screen::FlagMouseText;
                 screen->putc(fc - 0x80, cursor, flags);
             } $advance_if

             | 0xe0 .. 0xff ${
                 /* special inverse/normal */
                 uint8_t flags = _context.flags ^ Screen::FlagInverse;
                 screen->putc(fc - 0x80, cursor, flags);
             } $advance_if
             
    )* $err{ fgoto main; };
    
    write data;
}%%

@implementation GSOSConsole

+(void)load
{
    [EmulatorManager registerClass: self];
}

+(NSString *)name
{
    return @"GS/OS Console";
}

-(NSString *)name
{
    return @"GS/OS Console";
}

-(const char *)termName
{
    return "gsos-console";
}



-(BOOL)resizable
{
    return NO;
}

-(struct winsize)defaultSize
{
    struct winsize ws = { 24, 80, 0, 0 };
    
    return ws;
}

-(void)initTerm: (struct termios *)term
{
    // Control-U is used by the up-arrow key.
    term->c_cc[VKILL] = CTRL('X');
    // \t is a NOP so expand to spaces.
    term->c_oflag |= OXTABS;
}

-(id)init
{
    if ((self = [super init]))
    {
        [self reset: YES];
    }
    
    return self;
}

-(void)reset: (BOOL)hard
{
    
    %%write init;

    if (hard) {
        _context_stack.clear();

        _context.window = iRect(0, 0, 80, 24);
        _context.cursor = iPoint(0,0);
    }

    _context.consWrap = true;
    _context.consAdvance = true;
    _context.consLF = true;
    _context.consScroll = true;
    _context.consVideo = true;
    _context.consDLE = true;
    _context.consMouse = false;
    _context.consFill = 0xa0;

    _cursorType = Screen::CursorTypeUnderscore;

}


-(void)processData: (uint8_t *)data length: (size_t)length screen:(Screen *)screen output:(OutputChannel *)output
{
 
    const uint8_t *eof = nullptr;
    const uint8_t *p = data;
    const uint8_t *pe = data + length;

    auto &cursor = _context.cursor;
    auto &window = _context.window;
    %%write exec;
    screen->setCursor(cursor);
}

-(void)keyDown:(NSEvent *)event screen:(Screen *)screen output:(OutputChannel *)output
{
    NSEventModifierFlags flags = [event modifierFlags];
    NSString *chars = [event charactersIgnoringModifiers];
    
    NSUInteger length = [chars length];
    
    for (unsigned i = 0; i < length; ++i)
    {
        unichar uc = [chars characterAtIndex: i];
        
        switch (uc)
        {
            case NSEnterCharacter:
                output->write(CTRL('M'));
                break;
                
            // todo -- verify...
#if 0
             case NSDeleteCharacter:
                 output->write(0x7f);
                 break;
#endif
                
                // todo -- verify...
            case NSBackspaceCharacter:
                output->write(0x7f);
                break;
                
            case NSLeftArrowFunctionKey:
                output->write(CTRL('H'));
                break;
                
            case NSRightArrowFunctionKey:
                output->write(CTRL('U'));
                break;
                
            case NSUpArrowFunctionKey:
                output->write(CTRL('K'));
                break;
                
            case NSDownArrowFunctionKey:
                output->write(CTRL('J'));
                break;
                
                
            default:
                if (uc <= 0x7f)
                {
                    char c = uc;
                    if (flags & (NSShiftKeyMask | NSAlphaShiftKeyMask))
                    {
                        c = toupper(c);
                    }
                    if (flags & NSControlKeyMask)
                        c = CTRL(c);
                    
                    output->write(c);
                }
                break;
        }
    }
}

@end
