context: using System;
  using System.Linq;
  using System.Collections.Generic;
  using kOS.Safe.Persistence;
  using UnityEngine;
  using kOS.Safe.Screen;
  using kOS.Module;
  using kOS.UserIO;
  using kOS.Safe.UserIO;
  
  namespace kOS.Screen
  {
      // Blockotronix 550 Computor Monitor
      public class TermWindow : KOSManagedWindow , ITermWindow
      {
          private const int CHARSIZE = 8;
          private const string CONTROL_LOCKOUT = "kOSTerminal";
          private const int FONTIMAGE_CHARS_PER_ROW = 16;
          
          private static readonly string root = KSPUtil.ApplicationRootPath.Replace("\\", "/");
          private static readonly Color color = new Color(1, 1, 1, 1);
          private static readonly Color colorAlpha = new Color(0.9f, 0.9f, 0.9f, 0.6f);
          private static readonly Color bgColor = new Color(0.0f, 0.2f, 0.0f, 0.9f);
          private static readonly Color textColor = new Color(0.45f, 0.92f, 0.23f, 0.9f);
          private static readonly Color textColorAlpha = new Color(0.45f, 0.92f, 0.23f, 0.5f);
          private static readonly Color textColorOff = new Color(0.8f, 0.8f, 0.8f, 0.7f);
          private static readonly Color textColorOffAlpha = new Color(0.8f, 0.8f, 0.8f, 0.3f);
          private Rect closeButtonRect = new Rect(0, 0, 0, 0); // will be resized later.        
          private Rect resizeButtonCoords = new Rect(0,0,0,0); // will be resized later.
          private GUIStyle tinyToggleStyle;
          private Vector2 resizeOldSize;
          private bool resizeMouseDown;
          private bool consumeEvent;
          private bool keyClickEnabled;
          
          private bool collapseFastBeepsToOneBeep = false; // This is a setting we might want to fiddle with depending on opinion.
  
          private KeyBinding rememberThrottleCutoffKey;
          private KeyBinding rememberThrottleFullKey;
  
          private bool allTexturesFound = true;
          private CameraManager cameraManager;
          private float cursorBlinkTime;
          private Texture2D fontImage = new Texture2D(0, 0, TextureFormat.DXT1, false);
          private bool isLocked;
          /// <summary>How long blinks should last for, for various blinking needs</summary>
          private readonly TimeSpan blinkDuration = TimeSpan.FromMilliseconds(150);
          /// <summary>How long to pad between consecutive blinks to ensure they are visibly detectable as distinct blinks.</summary>
          private readonly TimeSpan blinkCoolDownDuration = TimeSpan.FromMilliseconds(50);
          /// <summary>At what milliseconds-from-epoch timestamp will the current blink be over.</summary>
          private DateTime blinkEndTime;
  
          /// <summary>Telnet repaints happen less often than Update()s.  Not every Update() has a telnet repaint happening.
          /// This tells you whether there was one this update.</summary>
          private bool telnetsGotRepainted;
          
          private Texture2D terminalImage = new Texture2D(0, 0, TextureFormat.DXT1, false);
          private Texture2D resizeButtonImage = new Texture2D(0, 0, TextureFormat.DXT1, false);
          private Texture2D networkZigZagImage = new Texture2D(0, 0, TextureFormat.DXT1, false);
          private WWW beepURL;
          private AudioSource beepSource;
          private int guiTerminalBeepsPending;
          
          private SharedObjects shared;
          private KOSTextEditPopup popupEditor;
          private Color currentTextColor = new Color(1,1,1,1); // a dummy color at first just so it won't crash before TerminalGUI() where it's *really* set.
  
          // data stored per telnet client attached:
          private readonly List<TelnetSingletonServer> telnets; // support exists for more than one telnet client to be attached to the same terminal, thus this is a list.
          private readonly Dictionary<TelnetSingletonServer, IScreenSnapShot> prevTelnetScreens;
          
          private ExpectNextChar inputExpected = ExpectNextChar.NORMAL;
          private int pendingWidth; // width to come from a resize combo.
          
          public string TitleText {get; private set;}
  
          private IScreenSnapShot mostRecentScreen;
          
          private DateTime lastBufferGet = DateTime.Now;
          private DateTime lastTelnetIncrementalRepaint = DateTime.Now;
  
          public TermWindow()
          {
              IsPowered = true;
              WindowRect = new Rect(50, 60, 0, 0); // will get resized later in AttachTo().
              telnets = new List<TelnetSingletonServer>();
              prevTelnetScreens = new Dictionary<TelnetSingletonServer, IScreenSnapShot>();
          }
  
          public bool ShowCursor { get; set; }
  
          public void Awake()
          {
              LoadTexture("GameData/kOS/GFX/font_sml.png", ref fontImage);
              LoadTexture("GameData/kOS/GFX/monitor_minimal.png", ref terminalImage);
              LoadTexture("GameData/kOS/GFX/resize-button.png", ref resizeButtonImage);
              LoadTexture("GameData/kOS/GFX/network-zigzag.png", ref networkZigZagImage);
  
              LoadAudio();
              
              tinyToggleStyle = new GUIStyle(HighLogic.Skin.toggle)
              {
                  fontSize = 10
              };
  
              var gObj = new GameObject( "texteditPopup", typeof(KOSTextEditPopup) );
              DontDestroyOnLoad(gObj);
              popupEditor = (KOSTextEditPopup)gObj.GetComponent(typeof(KOSTextEditPopup));
              popupEditor.SetUniqueId(UniqueId + 5);
          }
          
          private void LoadAudio()
          {
              beepURL = new WWW("file://"+ root + "GameData/kOS/GFX/terminal-beep.wav");
              AudioClip beepClip = beepURL.audioClip;            
              beepSource = gameObject.AddComponent<AudioSource>();
              beepSource.clip = beepClip;
          }
  
          public void LoadTexture(String relativePath, ref Texture2D targetTexture)
          {
              var imageLoader = new WWW("file://" + root + relativePath);
              imageLoader.LoadImageIntoTexture(targetTexture);
  
              if (imageLoader.isDone && imageLoader.size == 0) allTexturesFound = false;
          }
          
          public void OpenPopupEditor( Volume v, string fName )
          {
              popupEditor.AttachTo(this, v, fName );
              popupEditor.Open();
          }
          
          public override void GetFocus()
          {
              Lock();
         
  
          }
  
          public override void LoseFocus()
          {
              Unlock();
          }
  
          public override void Open()
          {
              base.Open();
              BringToFront();
              guiTerminalBeepsPending = 0; // Closing and opening the window will wipe pending beeps from the beep queue.
          }
  
          public override void Close()
          {
              // Diable GUI and release all locks
              base.Close();
          }
  
          public void Toggle()
          {
              if (IsOpen) Close();
              else Open();
          }
  
          private void Lock()
          {
              if (isLocked) return;
  
              isLocked = true;
              ShowCursor = true;
              BringToFront();
  
              cameraManager = CameraManager.Instance;
              cameraManager.enabled = false;
  
              InputLockManager.SetControlLock(CONTROL_LOCKOUT);
  
              // Prevent editor keys from being pressed while typing
              EditorLogic editor = EditorLogic.fetch;
                  //TODO: POST 0.90 REVIEW
              if (editor != null && InputLockManager.IsUnlocked(ControlTypes.All)) editor.Lock(true, true, true, CONTROL_LOCKOUT);
  
              // This seems to be the only way to force KSP to let me lock out the "X" throttle
              // key.  It seems to entirely bypass the logic of every other keypress in the game,
              // so the only way to fix it is to use the keybindings system from the Setup screen.
              // When the terminal is focused, the THROTTLE_CUTOFF action gets unbound, and then
              // when its unfocused later, its put back the way it was:
              rememberThrottleCutoffKey = GameSettings.THROTTLE_CUTOFF;
              GameSettings.THROTTLE_CUTOFF = new KeyBinding(KeyCode.None);
              rememberThrottleFullKey = GameSettings.THROTTLE_FULL;
              GameSettings.THROTTLE_FULL = new KeyBinding(KeyCode.None);
          }
  
          private void Unlock()
          {
              if (!isLocked) return;
              
              isLocked = false;
  
              InputLockManager.RemoveControlLock(CONTROL_LOCKOUT);
  
              cameraManager.enabled = true;
  
  
              EditorLogic editor = EditorLogic.fetch;
              if (editor != null) editor.Unlock(CONTROL_LOCKOUT);
  
              // This seems to be the only way to force KSP to let me lock out the "X" throttle
              // key.  It seems to entirely bypass the logic of every other keypress in the game:
              if (rememberThrottleCutoffKey != null)
                  GameSettings.THROTTLE_CUTOFF = rememberThrottleCutoffKey;
              if (rememberThrottleFullKey != null)
                  GameSettings.THROTTLE_FULL = rememberThrottleFullKey;
  
          }
  
          void OnGUI()
          {
              if (!IsOpen) return;
  
              if (isLocked) ProcessKeyEvents();
              
              try
              {
                  if (PauseMenu.isOpen || FlightResultsDialog.isDisplaying) return;
              }
              catch(NullReferenceException)
              {
              }
              
              GUI.skin = HighLogic.Skin;
              
              GUI.color = isLocked ? color : colorAlpha;
  
              // Should probably make "gui screen name for my CPU part" into some sort of utility method:
              ChangeTitle(CalcualteTitle());
  
              WindowRect = GUI.Window(UniqueId, WindowRect, TerminalGui, TitleText);
              
              if (consumeEvent)
              {
                  consumeEvent = false;
                  Event.current.Use();
              }
  
          }
          
          void Update()
          {
              if (shared == null || shared.Vessel == null || shared.Vessel.parts.Count == 0)
              {
                  // Holding onto a vessel instance that no longer exists?
                  Close();
              }
              GetNewestBuffer();
              TelnetOutputUpdate();
              ProcessTelnetInput(); // want to do this even when the terminal isn't actually displaying.
              if (telnetsGotRepainted)
              {
                  // Move the beeps from the screenbuffer "queue" to my own local terminal "queue".
                  // This is only done in the Update() when telentRepaint got triggered
                  // because otherwise the GUI terminal could wipe shared.Screen.BeepsPending to zero during an update
                  // where the telnet terminal never saw the beeps pending and never knew to print the beeps out.
                  guiTerminalBeepsPending += shared.Screen.BeepsPending;
                  shared.Screen.BeepsPending = 0; // Presume all the beeping has been dealt with for both the telnets and the GUI terminal.
              }
  
              if (!IsOpen ) return;
           
              UpdateLogic();
              UpdateGUIBeeps();
  
              if (!isLocked) return;
  
              cursorBlinkTime += Time.deltaTime;
              if (cursorBlinkTime > 1) cursorBlinkTime -= 1;
          }
          
          void UpdateGUIBeeps()
          {
              // Eat just one beep off the pending queue.  Wait for a future
              // Update() to consume more, if there are any more:
              if (guiTerminalBeepsPending > 0)
              {
                  // Behavior depending on whether we'd like BEEP, BEEP, BEEP to
                  // emit 3 back to back beeps, or just one beep ingoring overlapping beeps.
                  // Many hardware terminals only emit one beep when given a string
                  // of lots of beeps.
                  if (collapseFastBeepsToOneBeep)
                  {
                      Beep();
                      guiTerminalBeepsPending = 0;
                  }
                  else
                  {
                      if (Beep())
                          --guiTerminalBeepsPending;
                  }
              }
          }
          
          /// <summary>
          /// Attempts to make Unity start a playthrough of the beep audio clip.  The playthrough will continue
          /// on its own in the background while the rest of the code continues on.  It will do nothing if the previous
          /// beep is still being played.  (We only gave ourselves one audio beep source per GUI terminal so we can't play beeps
          /// simultaneously.)<br/>
          /// Addendum: It will redirect into a visual beep if that is called for instead.
          /// </summary>
          /// <returns>true if it beeped.  false if it coudn't beep yet (because the audio source is still busy emitting the previous beep).</returns>
          bool Beep()
          {
              if (shared.Screen.VisualBeep)
              {
                  DateTime nowTime = DateTime.Now;
                  if (nowTime < (blinkEndTime + (blinkCoolDownDuration))) // prev blink not done yet.
                      return false;
                  
                  // Turning this timer on tells GUI repainter elsewhere in this class to paint in reverse until it expires:
                  blinkEndTime = nowTime + blinkDuration;
              }
              else
              {
                  if (!beepSource.clip.isReadyToPlay || beepSource.isPlaying)
                      return false; // prev beep sound still is happening.
                  
                  // This is nonblocking.  Begins playing sound in background.  Code will not wait for it to finish:
                  beepSource.Play();
              }
              return true;
          }
  
          void TelnetOutputUpdate()
          {
              DateTime newTime = DateTime.Now;
              telnetsGotRepainted = false;
              
              // Throttle it back so the faster Update() rates don't cause pointlessly repeated work:
              // Needs to be no faster than the fastest theoretical typist or script might change the view.
              if (newTime > lastTelnetIncrementalRepaint + TimeSpan.FromMilliseconds(50)) // = 1/20th second.
              {
                  lastTelnetIncrementalRepaint = newTime;
                  foreach (TelnetSingletonServer telnet in telnets)
                  {
                      RepaintTelnet(telnet, false); // try the incremental differ update.
                  }
                  telnetsGotRepainted = true;
              }
          }
  
          /// <summary>
          /// Process the GUI event handler key events that are being seen the by GUI terminal,
          /// by translating them into values from the UnicodeCommand enum first if need be,
          /// and then passing them through to ProcessOneInputChar().
          /// </summary>
          void ProcessKeyEvents()
          {
              Event e = Event.current;
              if (e.type == EventType.KeyDown)
              {
                  // Unity handles some keys in a particular way
                  // e.g. Keypad7 is mapped to 0xffb7 instead of 0x37
                  var c = (char)(e.character & 0x007f);
  
                  // command sequences
                  if (e.keyCode == KeyCode.C && e.control) // Ctrl+C
                  {
                      ProcessOneInputChar((char)UnicodeCommand.BREAK, null);
                      consumeEvent = true;
                      return;
                  }
                  // Command used to be Control-shift-X, now we don't care if shift is down aymore, to match the telnet expereince
                  // where there is no such thing as "uppercasing" a control char.
  
gt:                 if ((e.keyCode == KeyCode.X && e.control) ||