{
    This file is part of Chentrah,
    Copyright (C) 2004-2008 Anton Rzheshevski (chebmaster@mail.ru).

    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 2 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/

 ********************************************************************** 

    This unit contains the built-in game module
      (i.e. a set of procedures that handle the error
       and warning messages and module selection menu)

 **********************************************************************}


{$include mo_globaldefs.h}

unit cl_builtinmodule;

{  Один мудрый человек заметил,
     что модуль в Паскале - эквивалент синглтона в Си++}

interface

  function BimPulse(): longbool;
  procedure BimOnMouseMove(mx, my: integer);


  procedure BimSetModuleError(cap, msg: WideString);
  procedure BimSetModuleMenu;
  
  type
    TbimState = (
     bimsInvalid, bimsChooseLang, bimsStartupError, bimsWarning,
     bimsModuleError, bimsModuleFatalError, bimsModuleMenu,
     bimsAckSessionErasure);

  var
    bimState: TbimState = bimsModuleMenu;
    prevState: TbimState = bimsInvalid;
    ModuleSelectionMenuRequested: boolean = false;

implementation

uses
  SysUtils, typinfo, cge, un_randoms, cl_strings, math
       {$ifdef buildmein}
       , cl_internalmodule
       {$endif} ;


  const
    color: array[0..3] of GLfloat = (0.8, 0.8, 0.8, 1); // the text and buttons
    color_selected: array[0..3] of GLfloat = (0.5, 1, 1, 1);
    color_exit: array[0..3] of GLfloat = (1, 0.7, 0.8, 1); //the exit button. Slowly flashes pink.

    NormalFontSize = 18;//38;
    
    LINE_LENGTH_LIMIT = 60;
    BUTTONS_DISABILITY_TIME = FADE_IN_TIME * 0.66; {when screen fades to black, the buttons
      stop operating to prevent user from blindly clicking forward}
    
    ACKNOWLEDGE_BUTTONS_NUM = 8; {for the session erase menu. One, randomlu chosen, is Yes, another seven are No}
    
  var
    buttons, text: array of WideString;
    caption, b_message: WideString;
    btop, brand: array of integer;
    bleft,
    ButtonPanelWidth,
    ButtonHeight: integer;
    SelectedButton: integer = -1;
    ClickedButton: integer = -1;
    B_border: integer;
    TextLen,
    TextWidth,
    CaptionHeight,
    SeparatorWidth,
    SeparatorHeight: integer;
    lwd: GLfloat;
//    SwapMoment: TDateTime = 0.0;
    StartingMoment: TDateTime = 0.0;
    time: double;
    mesn: integer = 0;
    b_inipar: string;
    b_inival: integer;
    asa: integer = 0;

  procedure SetText(w: WideString);
  var
    i: integer;
  begin
    setlength(text, 1);
    text[0]:='';
    for i:=1 to length(w) do
      case w[i] of
        #0..#12, #14..#31:;
        #13: setlength(text, length(text) + 1);
      else
        text[high(text)] += w[i];
      end;
    AddLog(RuEn('bim диалог:','bim dialog:') + #10#13'%0'#10#13'%1',  [Caption, w]);
  end;


  procedure SetMessage(var M: TBimMessage);
  begin
    if M.capStr = ''
      then caption:= MessageContainer[M.capMID]
      else caption:= M.capStr;
    if M.msgStr = ''
      then SetText(PervertedFormatW(MessageContainer[M.msgMID], M.params))
      else SetText(PervertedFormatW(M.msgStr, M.params));
    b_inipar:= M.inipar;
    b_inival:= M.writeval;
  end;



  procedure React;
  begin
    if ClickedButton < 1 then exit;
    if time < BUTTONS_DISABILITY_TIME then exit;
    try
      AddLog(RuEn('Пользователь щёлкнул по кнопке %0','User clicked "%0"'),[buttons[ClickedButton]]);
      case bimState of
        bimsModuleMenu: begin
          prevState:= bimsInvalid;
          MotherState.BlackoutStart:= Now();
          Module.Reload(ClickedButton);
        end;
        bimsChooseLang: begin
          MessageContainer.SetLanguage(ClickedButton - 1);
          bimState:= bimsModuleMenu;
          if length(StopMessage) = 0 then MotherState.RestartRequested:= Yes; {
            Otherwise things are screwed on the following screens.
            Module names are not loaded correctly (as Chentra does it prior to displaying the language dialog,
              and doesn't reload them. Also, pen tablet status and other things are reported in English even
              if user chooses Russian.
          }
        end;
        bimsStartupError: begin
          inc(mesn);
          if mesn = high(StopMessage) then SetLength(buttons, 1); //hide the "Next >>" burtton
          SetMessage(StopMessage[mesn]);
          MotherState.BlackoutStart:= Now();
          ClickedButton:= -1;
        end;
        bimsWarning: begin
          if b_inipar <> '' then Config.Int['warnings', b_inipar]:= b_inival;
          inc(mesn);
          if mesn > high(WarningMessage) then begin
            bimState:= bimsModuleMenu;
            SetLength(WarningMessage, 0);
            Module.Reload(Module.GetDefaultNum)
          end
          else begin
            SetMessage(WarningMessage[mesn]);
            if WarningMessage[mesn].isyellow
              then Console.SetDefaultBg(dbWarning)
              else Console.SetDefaultBg(dbTitle);
            MotherState.BlackoutStart:= Now();
          end;
          ClickedButton:= -1;
        end;
        bimsModuleError
       {$ifndef buildmein}
                       : begin
          if ClickedButton = 1 then bimState:= bimsModuleMenu;
          if ClickedButton = 2 then Module.Reload(Module.GetDefaultNum);
          if ClickedButton = 3 then MotherState.RestartRequested:= Yes;
          if ClickedButton = 4 then begin
            bimState:= bimsAckSessionErasure;
            asa:=0;
          end;
          MotherState.BlackoutStart:= Now;
        end;
       {$else}
        ,
       {$endif}
        bimsModuleFatalError: MotherState.RestartRequested:= Yes;
        bimsAckSessionErasure: begin
          //The most annoying menu. Asks you five times if you are sane, aren't brainwashed, etc.
          if (ClickedButton > 0) and (brand[ClickedButton - 1] = 0) then begin
            inc(asa);
            if asa > 4 then begin
              asa:= 0;
              bimState:= bimsModuleMenu;
              MotherState.BlackoutStart:= Now();
              Module.EraseSession(Module.GetDefaultNum);
              Module.Reload(Module.GetDefaultNum);
            end
            else
              prevState:= bimsInvalid;
          end
          else begin
            asa:= 0;
            bimState:= bimsModuleMenu;
          end;
        end;

      else
        Die(MI_ERROR_PROGRAMMER_NO_BAKA, ['cl_builtinmodule.React() :Invalid state ' + GetEnumName(typeinfo(TbimState), ord(bimState))]);
      end;
      if Assigned(SoundManager) then SoundManager.PlayWav('click.wav');
    except
      Die('Crashed in cl_builtinmodule.React()')
    end;
  end;

  const asmi: array[0..4] of TMessageID = (
    MI_ACK_SESSION_ERASE_1,
    MI_ACK_SESSION_ERASE_2,
    MI_ACK_SESSION_ERASE_3,
    MI_ACK_SESSION_ERASE_4,
    MI_ACK_SESSION_ERASE_5);

  procedure SwapState;
  var
    i, iy: integer;
    an: TMessageID;
    yy: boolean;
  begin
    if prevState = bimState then exit;
    try
      MotherState.BlackoutStart:= Now();
      SetLength(text, 0);
      SetLength(buttons, 1);
      buttons[0]:= MessageContainer[MI_EXIT];
      ClickedButton:= -1;
      mesn:= 0;
      case bimState of
        bimsModuleMenu: begin
          MotherState.RollBackTheSessionOnNextLoad:= False;
          Caption:= MessageContainer[MI_MODULE_MENU_CAPTION];
         {$ifdef buildmein}
          setlength(buttons, 2);
          buttons[1]:= MyInternalModuleName;
          SetText(MessageContainer[MI_MODULE_MENU_TEXT_BUILTIN]);
         {$else}
          setlength(buttons, Length(Module.MList) + 1);
          for i:= 1 to Length(Module.MList) do begin
            buttons[i]:= Module.MList[i - 1].Name;
          end;
          SetText(MessageContainer[MI_MODULE_MENU_TEXT]);
         {$endif}
          Console.SetDefaultBg(dbTitle);
        end;
        bimsModuleError
        {$ifndef buildmein}
                        : begin
          SetMessage(ModuleStopMessage);
          SetLength(buttons, 5);
          buttons[1]:=MessageContainer[MI_ME_MOTHER_MENU];
          buttons[2]:=MessageContainer[MI_ME_RESTART_MODULE];
          buttons[3]:=MessageContainer[MI_ME_RESTART_PROGRAM];
          buttons[4]:=MessageContainer[MI_ME_ERASE_SESSION];
          Console.SetDefaultBg(dbError);
        end;
       {$else}
        ,
       {$endif}
        bimsModuleFatalError: begin
          SetMessage(ModuleStopMessage);
          SetLength(buttons, 2);
          buttons[1]:=MessageContainer[MI_ME_RESTART_PROGRAM];
          Console.SetDefaultBg(dbError);
        end;
        bimsWarning: begin
          SetMessage(WarningMessage[0]);
          SetLength(buttons, 2);
          buttons[1]:=MessageContainer[MI_NEXT];
          if WarningMessage[0].isyellow
            then Console.SetDefaultBg(dbWarning)
            else Console.SetDefaultBg(dbTitle);
        end;
        bimsStartupError: begin
          if Assigned(SoundManager) then SoundManager.PlayWav('oops.wav');
          SetMessage(StopMessage[0]);
          if length(StopMessage) > 1 then begin
            SetLength(buttons, 2);
            buttons[1]:=MessageContainer[MI_NEXT];
          end;
          Console.SetDefaultBg(dbError);
        end;
        bimsChooseLang: begin
          SetLength(buttons, 1 + MessageContainer.NumLanguages);
          for i:= 1 to MessageContainer.NumLanguages do
            buttons[i]:=MessageContainer.GetMessage(i - 1, MI_LANGUAGE_NAME);
          caption:= MessageContainer[MI_CHOOSE_LANG_CAPTION];
          if MotherState.InstallPath <> '' then
            SetText(PervertedFormat(MessageContainer[MI_CHOOSE_LANG_TEXT],
                                    [GetHotkeyName(LanguageHotKey)]));
          if length(StopMessage) > 0
            then Console.SetDefaultBg(dbError)
            else begin
              yy:= false;
              for iy:= 0 to high(WarningMessage) do
                if warningMessage[iy].isyellow then yy:= true;
              if yy then Console.SetDefaultBg(dbWarning)
                    else Console.SetDefaultBg(dbTitle);
            end;

        end;
        bimsAckSessionErasure: begin
          SetLength(brand, ACKNOWLEDGE_BUTTONS_NUM);
          SetLength(buttons, ACKNOWLEDGE_BUTTONS_NUM + 1);
          RandomSeqI(brand);
          for i:=0 to high(brand) do
            buttons[i + 1]:= StrParm(MessageContainer[MI_ACK_BUTTONS], brand[i] + 1, ',');
          SetText(MessageContainer[asmi[asa]]);
        end;
      else
        Die(MI_ERROR_PROGRAMMER_NO_BAKA, ['cl_builtinmodule.SwapState() :Invalid state ' + GetEnumName(typeinfo(TbimState), ord(bimState))]);
      end;
      SetLength(btop, length(buttons));
      prevState:= bimState;

    except
      addlog('ыпс..');
      Die('Crashed in cl_builtinmodule.SwapState()')
    end;
  end;

  procedure CalcSizes;
  var
    i: integer;
  begin
    ButtonPanelWidth:=round(0.3 * MotherState.DisplayWidth);//min(300, round(0.3 * WindowManager.DisplayWidth));
    SeparatorWidth:= max(10, round(MotherState.DisplayWidth * 0.07));
    SeparatorHeight:= max(10, round(MotherState.DisplayHeight * 0.07));
    
    ButtonHeight:=min(76, round((MotherState.DisplayHeight - 2 * SeparatorHeight) div (length(buttons) + 1)));
    B_border:=max(5, round(ButtonHeight * 0.25));
    SetLength(btop, length(buttons));
    


    bleft:=MotherState.DisplayWidth - ButtonPanelWidth;
    btop[0]:= SeparatorHeight;
    for i:=1 to high(buttons)
      //do btop[i]:= SeparatorHeight + ButtonHeight * (length(buttons) - i);
      do btop[i]:= btop[0] + ButtonHeight * (i + 1);

    TextLen:= 10;
    for i:=0 to high(text) do TextLen:=max(TextLen, min(LINE_LENGTH_LIMIT, Length(text[i])));
    

    TextWidth:= bleft - 2 * SeparatorWidth;
    CaptionHeight:= min(50, round(MotherState.DisplayHeight * 0.15));
    
    lwd:=1.5;//min(2.0, max(0.0, (b_border / 3 - 1.0));
    
  end;
  
  procedure RenderButton(n: integer);
  var
    x0, x1, x2, x3, y0, y1, y2, y3: GLfloat;
    bev: integer;
  begin
    bev:=max(1, min(3, round(b_border * (3/10))));
    x0:=bleft;
    x1:=x0 + bev;
    x3:=MotherState.DisplayWidth - SeparatorWidth;
    x2:=x3 - bev;
    y0:= btop[n] + b_border div 2;
    y1:= y0 + bev;
    y3:= y0 + ButtonHeight - B_border;
    y2:= y3 - bev;
    glDisable(GL_TEXTURE_2D);
    glBegin(GL_LINE_LOOP);
      glVertex2f(x1, y0);
      glVertex2f(x2, y0);
      glVertex2f(x3, y1);
      glVertex2f(x3, y2);
      glVertex2f(x2, y3);
      glVertex2f(x1, y3);
      glVertex2f(x0, y2);
      glVertex2f(x0, y1);
    glEnd();
  end;
  
  procedure Render;
  var
    i, th, thc, ttop, bw, bh: integer;
    fit: TStringFitRec;
  begin
    try
      color_exit[1]:= 0.75 + 0.25 * sin(1.7*(Now() * 86400));
      color_exit[2]:= 0.75 + 0.25 * sin(1.7*(Now() * 86400 + 0.07));
      
      if (bimState in [bimsStartupError, bimsModuleError, bimsModuleFatalError, bimsAckSessionErasure])
      or ((bimState = bimsChooseLang) and (length(StopMessage) > 0))
      then begin
        color[0]:=1; //yellow, since the background image is dark red
        color[1]:=1;
        color[2]:=0.3;
      end
      else begin
        color[0]:=0.8;//light gray since the background is gray //light bluish-gray, since the background image is violet
        color[1]:=0.8;//0.9;
        color[2]:=0.8;//0.9;
      end;

      //render the buttons

      glDisable(GL_DEPTH_TEST);
      for i:=0 to high(buttons) do begin
        glDisable(GL_TEXTURE_2D);
        glEnable(GL_LINE_SMOOTH);
        glEnable(GL_BLEND);
        glDisable(GL_ALPHA_TEST);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

        glLineWidth(3 + lwd);
        glColor4f(0, 0, 0, 0.6 * MotherState.fadein);
        RenderButton(i);

        if i = SelectedButton
          then glColor4f(color_selected[0]*MotherState.fadein, color_selected[1]*MotherState.fadein, color_selected[2]*MotherState.fadein, MotherState.fadein)
          else
            if i=0 then glColor4f(color_exit[0]*MotherState.fadein, color_exit[1]*MotherState.fadein, color_exit[2]*MotherState.fadein, MotherState.fadein)
                   else glColor4f(color[0]*MotherState.fadein, color[1]*MotherState.fadein, color[2]*MotherState.fadein, MotherState.fadein);
        glLineWidth(1 + lwd);
        RenderButton(i);

        _cgeffSetRenderState(false);
        _cgeffFitStringIntoRectangle(
           min(NormalFontSize, ButtonHeight),
           length(buttons[i]), 1,
           ButtonPanelWidth - 2 * B_border - SeparatorWidth,
           ButtonHeight - 2* B_border, @fit);
        _cgeffRenderString(@fit,
           bleft + B_border + (ButtonPanelWidth - 2 * B_border - SeparatorWidth - fit.ActualWidth) div 2,
           btop[i] + B_border + (ButtonHeight - 2 * B_border - fit.ActualHeight) div 2,
           PWideChar(buttons[i]));
      end;

      //render the caption

      _cgeffSetRenderState(false);
      glColor4f(color[0]*MotherState.fadein, color[1]*MotherState.fadein, color[2]*MotherState.fadein, MotherState.fadein);
      _cgeffFitStringIntoRectangle(
           round(CaptionHeight * 0.7),
           length(caption), 1,
           bleft - CaptionHeight - 2 * SeparatorWidth,
           CaptionHeight, @fit);
      _cgeffRenderString(@fit,
        SeparatorWidth,// + (bleft - CaptionHeight - 2 * SeparatorWidth - aw) div 2,
        SeparatorHeight + (Captionheight - fit.ActualHeight) div 2,
        PWideChar(caption));


       //render the text
         //..first, calculate the line height
         //  assuming them lines are no longer than 80 characters
         // we really need only ActualHeight from this call.
      _cgeffFitStringIntoRectangle(
           NormalFontSize,
           TextLen, length(text),
           bleft - 2 * SeparatorWidth,
           MotherState.DisplayHeight - CaptionHeight - 3*SeparatorHeight,
           @fit);
      th:= fit.ActualHeight div max(1, length(text));
      ttop:= CaptionHeight + 2*SeparatorHeight;

      //then actually render the text, autofitting each line individually
      //  (ugly when lines are too long - but readable!)
      for i:=0 to high(text) do begin
        _cgeffFitStringIntoRectangle(
             NormalFontSize,
             length(text[i]), 1,
             bleft - 2 * SeparatorWidth,
             th,
             @fit);
        _cgeffRenderString(@fit,
          SeparatorWidth,
          ttop + i*th + (th - fit.ActualHeight) div 2,
          PWideChar(text[i]));
      end;

    except
      Die('Crashed in cl_builtinmodule.Render()')
    end;

  end;
  
  procedure ChooseButton(mx, my: integer);
  var
    i: integer;
  begin
    SelectedButton:= -1; //Math.Min(1, high(buttons)); //One button is always selected, to allow navigating by game controller or keyboard
    for i:=0 to high(buttons) do
      if     (mx + SeparatorWidth > bleft + b_border)
         and (my > btop[i] + b_border div 2)
         and (my < btop[i] + ButtonHeight - b_border div 2)
      then begin
        SelectedButton:=i;
        break;
      end;
  end;

  procedure ChooseButtonByKeyboard(dv: integer);
  begin
    if SelectedButton < 0 then SelectedButton:= 1;
    SelectedButton+= dv;
    SelectedButton:= Math.max(Math.min(0,high(buttons)), Math.Min(SelectedButton, high(buttons)));
  end;

  procedure BimOnMouseMove(mx, my: integer);
  begin
    ChooseButton(mx, my);
  end;
  
  procedure BimOnPress(key: TKey; mx, my: integer);
  begin
    ChooseButton(mx, my);
    case key of
      KEY_ESCAPE, KEY_GAMEPAD_B: MotherState.ExitRequested:= Yes;
      KEY_MOUSE_LEFT, KEY_GAMEPAD_A, KEY_GAMEPAD_RIGHT_SHOULDER, KEY_GAMEPAD_RIGHT_THUMB, KEY_ENTER: ClickedButton:= SelectedButton;
      KEY_UP, KEY_GAMEPAD_DPAD_UP: ChooseButtonByKeyboard(-1);
      KEY_DOWN, KEY_GAMEPAD_DPAD_DOWN: ChooseButtonByKeyboard(1);
    end;
//addlog(' %0  %1 ', [mx, WindowManager.DisplayHeight - my]);
  end;

  var prev_jy: float = 0.0;
      prev_jdy: float = 0.0;

  procedure BimProcessMessages;
  var
    i, j, t, c, mx, my, yi, dyi: integer;
    jy, jdy: float;
    e: PInputEvent;
  begin
    e:= PInputEvent(MotherState.InputEvents);
    if not Assigned(e) then Exit;
    while e^.EventType <> 0 do begin
      case e^.EventType of
        CGM_PRESS: BimOnPress(e^.key, Trunc(e^.mx), Trunc(e^.my));
        CGM_RELEASE:;// BimOnRelease(TKey(c), mx, my);
        CGM_TYPE:;// BimOnType(WideChar(word(c)), mx, my);
        CGM_MOUSEMOVE: begin
          BimOnMouseMove(Trunc(e^.mx), Trunc(e^.my));
        end;
        CGM_JOYSTICK: begin
          if e^.JoyType = JoytypePrimary then begin
            jy:= e^.JoyY;
            //jy+= e^.JoydY * 0.3; Filter da noise out first! This shit glitches!
            if (jy > 0.4) and (jy > prev_jy) then begin
              prev_jy:= jy * 1.3;
              ChooseButtonByKeyboard(-1);
            end;
            if (jy > 0) and (jy < prev_jy * 0.6) then prev_jy:=jy;
            if (jy < -0.4) and (jy < prev_jy) then begin
              prev_jy:= jy * 1.3;
              ChooseButtonByKeyboard(1);
            end;
            if (jy < 0) and (jy > prev_jy * 0.6) then prev_jy:=jy;
          end;
        end
      else
        ; //silently ignore all other events
      end;
      inc(e);
    end;
    SetLength(MotherState.InputEvents, 0);
  end;



  procedure CheckState;
  begin
    if MessageContainer.CurrentLanguage < 0
      then bimState:= bimsChooselang
      else
        if Length(StopMessage) > 0
          then bimState:= bimsStartupError
          else
            if Length(WarningMessage) > 0
              then bimState:= bimsWarning;
  end;

  procedure BasicAdjustQF;
  //uses average duration of call to xxxSwapBuffers() as a rough estimate of videocard's abilities to handle pixel/texel load.
  var OptimalQf: float;
  begin
    // at 16000 nanoseconds and 60 FPS, the call to SwapBuffers would take almost entire frame time.
    // at 64000, the videocard couldn't even keep 30 FPS (cough mesa software mode in Debian cough)
    // 6-8 thousands us is meh (your average videocard with Aero transparent windows overlapping our own)
    OptimalQf:=  (640000.0 / MotherState.AverageSwapBuffersDuration);

    with MotherState do begin
      if (QF > OptimalQF) and (QF > QF_MIN) then QF-= 0.1;
      if (QF < (OptimalQF / 2)) and (QF < 99) then QF+= 0.03;
    //never raise it over 99. Because, you know, this estimate here is *rough*.
    end;
  end;


  function BimPulse(): longbool;
  begin
    Result:=Yes;
    CheckState;
//DbgSay(GetEnumName(typeinfo(TbimState), ord(bimState)) + ' '+ IntToStr(round(time * 100)));
    try
      WindowManager.UpdateWindowSize;
      SwapState;
      CalcSizes;
      BimProcessMessages;
      time:= (Now() - MotherState.BlackoutStart) * 84600;
//      MotherState.fadein:= FancyFunc( time / FADE_IN_TIME);
      with MotherState do glColor4f(fadein, fadein, fadein, 1);
//      Console.SetFadeIn(MotherState.fadein);
      Console.DrawBackground();
      Render;
      React;
      if ClickedButton = 0 then MotherState.ExitRequested:= true;
      BasicAdjustQF();
    except
      try
        Die(RuEn('Рухнул встроенный модуль.','The built-in module crashed.'));
      except
      end;
      Result:= No;
    end;
  end;



  function BimSave(): boolean; cdecl;
  begin
    Result:= Yes;
  end;

  function BimFree(): boolean; cdecl;
  begin
    Result:= Yes;
  end;

  procedure BimSetGetAddressCallbackProc(Addr: pointer); cdecl;
  begin

  end;

  procedure BimSetModuleError(cap, msg: WideString);
  begin
    caption:= cap;
    b_message:= msg;

    bimState:=bimsModuleError;
  end;

  procedure BimSetModuleMenu;
  begin
    Console.SetDefaultBg(dbTitle);
  end;
  

end.
