/* -*- Mode: Vala; indent-tabs-mode: nil; c-basic-offset: 2; tab-width: 2 -*-
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 * SPDX-FileCopyrightText: Michael Terry
 */

using GLib;

public enum CanvasMode {NONE, DIGIT, CHAR}

// This is a stupid little construction that vala needs to get lengths right
struct ArrayBox {
  public Gtk.Widget[] array;
}

public class Canvas : Gtk.Box
{
  public MultPuzzle puzzle { get; construct; }
  public GtkMult mult { get; construct; }
  
  public Canvas (GtkMult mult)
  {
    Object(mult: mult);
  }
  
  public void clear_mode ()
  {
    set_mode (CanvasMode.NONE, null);
  }
  
  private string mode_letter;
  private CanvasMode mode;
  
  public void set_mode (CanvasMode m, CharBox? box)
  {
    mode = m;
    mode_letter = box == null ? null : box.letter ;
    
    foreach (CharBox b in mult_boxes)
      highlight_box (b, false, false, false);
    foreach (CharBox b in digit_boxes)
      highlight_box (b, true, false, false);

    if (mode != CanvasMode.NONE) {
      mult.set_feedback("%s = ?".printf(_(mode_letter)), true);
    }
  }
  
  public void start_choice (char ch)
  {
    // First, see if digit or letter, then find box
    weak List<CharBox> boxes = ch.isdigit() ? digit_boxes : mult_boxes;
    string str = "%c".printf(ch);
    foreach (CharBox b in boxes)
      if (str == b.letter) {
        if (ch.isdigit())
          start_digit_choice (b);
        else
          start_char_choice (b);
        break;
      }
  }
  
  public void start_digit_choice (CharBox b)
  {
    if (mode == CanvasMode.CHAR) // this is really an end-char-choice, not a start-digit-choice
    {
      if (b.get_state_flags() != Gtk.StateFlags.INSENSITIVE)
        puzzle.guess (b.letter[0].digit_value (), (MultPuzzleChar)mode_letter[0]);
      clear_mode ();
    }
    else
      set_mode (CanvasMode.DIGIT, b);
  }
  
  public void start_char_choice (CharBox b)
  {
    if (mode == CanvasMode.DIGIT) // this is really an end-digit-choice, not a start-char-choice
    {
      if (b.get_state_flags() != Gtk.StateFlags.INSENSITIVE)
        puzzle.guess (mode_letter[0].digit_value (), (MultPuzzleChar)b.letter[0]);
      clear_mode ();
    }
    else
      set_mode (CanvasMode.CHAR, b);
  }
  
  private int num_box_rows;
  private int num_box_cols; // Not counting two digit cols
  
  private Gtk.Grid symbol_grid;
  Gtk.AspectFrame digit_aspect;
  private Gtk.Grid digit_grid;
  
  private List<CharBox> mult_boxes;
  private List<CharBox> digit_boxes;
  
  private Gtk.Widget[] xboxes;
  private Gtk.Widget[] yboxes;
  private Gtk.Widget[] zboxes;
  private ArrayBox[] addboxes;
  private Gtk.Widget[] digitboxes;

  construct
  {
    this.orientation = Gtk.Orientation.HORIZONTAL;
    this.spacing = 18;
    this.halign = Gtk.Align.CENTER;
    this.valign = Gtk.Align.CENTER;

    this.puzzle = mult.puzzle;
    this.mode = CanvasMode.NONE;
    this.mode_letter = null;
    this.mult_boxes = null;
    this.digit_boxes = null;
    
    string x = this.puzzle.get_multiplicand();
    string y = this.puzzle.get_multiplier();
    string z = this.puzzle.get_answer();
    this.num_box_rows = 3 + (int)y.length;
    this.num_box_cols = 1 + (int)z.length;
    
    this.xboxes = new Gtk.Widget[x.length];
    this.yboxes = new Gtk.Widget[y.length];
    this.zboxes = new Gtk.Widget[z.length];
    this.digitboxes = new Gtk.Widget[10];
    
    this.addboxes = new ArrayBox[this.puzzle.get_num_addends()];
    for (int i = 0; i < this.addboxes.length; ++i)
      this.addboxes[i].array = new Gtk.Widget[this.puzzle.get_addend(i).length];
    
    var click = new Gtk.GestureClick();
    click.released.connect(() => handle_none_release());
    add_controller(click);

    handle_puzzle_change(this.puzzle);
    this.puzzle.changed.connect(handle_puzzle_change);
  }

  private Gtk.Widget create_mult_box(unichar ch)
  {
    if (ch.isdigit ()) {
      var b = new ScalingLetter("%c".printf((char)ch));
      b.add_css_class("numeric");
      return b;
    }
    else {
      var b = new TableBox((char)ch, this);
      b.add_css_class("monospace");
    
      var motion = new Gtk.EventControllerMotion();
      motion.enter.connect(() => highlight_box(b, false, true, true));
      motion.leave.connect(() => highlight_box(b, false, false, true));
      b.add_controller(motion);

      b.clicked.connect(() => handle_release(b, false));
      
      var aspect = new Gtk.AspectFrame(0.5f, 0.5f, 1.0f, false);
      aspect.child = b;

      mult_boxes.append(b);
      return aspect;
    }
  }
  
  private Gtk.Widget create_digit_box(bool[] unknown, int i)
  {
    if (unknown[i] == true) {
      var digit_str = "%i".printf(i);
      var b = new DigitBox(this, digit_str);
      b.add_css_class("numeric");
      
      var motion = new Gtk.EventControllerMotion();
      motion.enter.connect(() => highlight_box(b, true, true, true));
      motion.leave.connect(() => highlight_box(b, true, false, true));
      b.add_controller(motion);

      b.clicked.connect(() => handle_release(b, true));

      var aspect = new Gtk.AspectFrame(0.5f, 0.5f, 1.0f, false);
      aspect.child = b;

      digit_boxes.append(b);
      return aspect;
    }
    else
      return new ScalingLetter(" ");
  }
  
  private void handle_puzzle_change(MultPuzzle p)
  {
    // Create CharBoxes for all the pieces of the puzzle.

    // First clear out and recreate our grids
    if (this.symbol_grid != null) {
      remove(this.symbol_grid);
      remove(this.digit_aspect);
    }
    this.symbol_grid = new Gtk.Grid();
    this.symbol_grid.halign = Gtk.Align.FILL;
    this.symbol_grid.valign = Gtk.Align.FILL;
    this.symbol_grid.row_spacing = 6;
    this.symbol_grid.column_spacing = 6;
    append(this.symbol_grid);
    this.digit_grid = new Gtk.Grid();
    this.digit_grid.halign = Gtk.Align.FILL;
    this.digit_grid.valign = Gtk.Align.FILL;
    this.digit_grid.row_spacing = 6;
    this.digit_grid.column_spacing = 6;
    this.digit_grid.margin_top = 7; // same space as the separator takes up
    this.digit_grid.margin_bottom = 7;
    this.digit_grid.margin_start = 7;
    this.digit_grid.margin_end = 7;
    var digit_bin = new Adw.Bin();
    digit_bin.child = digit_grid;
    digit_bin.add_css_class("frame");
    this.digit_aspect = new Gtk.AspectFrame(0.5f, 0.5f, 1.0f, true);
    this.digit_aspect.child = digit_bin;
    append(this.digit_aspect);

    var size_group = new Gtk.SizeGroup(Gtk.SizeGroupMode.BOTH);

    this.mult_boxes = new List<CharBox>();
    this.digit_boxes = new List<CharBox>();

    // Add constant labels
    var times_letter = new ScalingLetter("×");
    this.symbol_grid.attach(times_letter, 0, 1, 1, 1);
    size_group.add_widget(times_letter);

    var plus_letter = new ScalingLetter("+");
    this.symbol_grid.attach(plus_letter, 0, this.addboxes.length + 2, 1, 1);
    size_group.add_widget(plus_letter);

    this.symbol_grid.attach(new Separator(), 0, 2, this.num_box_cols, 1);
    this.symbol_grid.attach(new Separator(), 0, this.addboxes.length + 3, this.num_box_cols, 1);

    var row = 0;
    var multiplicand = this.puzzle.get_multiplicand();
    for (int i = 0; i < multiplicand.length; ++i) {
      this.xboxes[i] = create_mult_box(multiplicand[i]);
      var xindex = this.num_box_cols - multiplicand.length + i;
      this.symbol_grid.attach(this.xboxes[i], xindex, row, 1, 1);
      size_group.add_widget(this.xboxes[i]);
    }
    row++;
    
    var multiplier = this.puzzle.get_multiplier();
    for (int i = 0; i < multiplier.length; ++i) {
      this.yboxes[i] = create_mult_box(multiplier[i]);
      var xindex = this.num_box_cols - multiplier.length + i;
      this.symbol_grid.attach(this.yboxes[i], xindex, row, 1, 1);
      size_group.add_widget(this.yboxes[i]);
    }
    row++;
    row++; // separator

    for (int i = 0; i < this.puzzle.get_num_addends(); ++i) {
      var addend = this.puzzle.get_addend(i);
      for (int j = 0; j < addend.length; ++j) {
        this.addboxes[i].array[j] = create_mult_box(addend[j]);
        var xindex = this.num_box_cols - addend.length - i + j;
        this.symbol_grid.attach(this.addboxes[i].array[j], xindex, row, 1, 1);
        size_group.add_widget(this.addboxes[i].array[j]);
      }
      row++;
    }
    row++; // separator

    var answer = this.puzzle.get_answer();
    for (int i = 0; i < answer.length; ++i) {
      this.zboxes[i] = create_mult_box(answer[i]);
      var xindex = this.num_box_cols - answer.length + i;
      this.symbol_grid.attach(this.zboxes[i], xindex, row, 1, 1);
      size_group.add_widget(this.zboxes[i]);
    }
    
    var unknown = this.puzzle.get_unknown_digits();
    for (int i = 0; i < 10; i++) {
      this.digitboxes[i] = create_digit_box(unknown, i);
      this.digit_grid.attach(this.digitboxes[i], i % 2, i / 2, 1, 1);
      size_group.add_widget(this.digitboxes[i]);
    }
    
    if (p.is_done) {
      this.sensitive = false;
      
      foreach (var b in this.digit_boxes)
        b.highlight = Gtk.StateFlags.INSENSITIVE;
    }
  }
  
  public void highlight_box (CharBox box, bool digit_box, bool hover, bool all)
  {
    if (all)
    {
      weak List<CharBox> boxes = digit_box ? digit_boxes : mult_boxes;
      foreach (CharBox b in boxes)
        if (b != box && b.letter == box.letter)
          highlight_box (b, digit_box, hover, false);
    }
    
    CanvasMode native_mode = digit_box ? CanvasMode.DIGIT : CanvasMode.CHAR;
    
    if (mode == native_mode)
    {
      if (mode_letter == box.letter)
      {
        box.highlight = Gtk.StateFlags.ACTIVE;
        return;
      }
    }
    else if (mode != CanvasMode.NONE)
    {
      if (digit_box) {
        var unknowns = puzzle.get_unknown_digits ();
        var guesses = puzzle.get_letter_guesses ((MultPuzzleChar)mode_letter[0]);
        int digit = box.letter[0].digit_value ();
        if (!unknowns[digit] || guesses[digit]) {
          box.highlight = Gtk.StateFlags.INSENSITIVE;
          return;
        }
      }
      else
      {
        int digit = mode_letter[0].digit_value ();
        if (puzzle.get_letter_guesses ((MultPuzzleChar)box.letter[0])[digit]) {
          box.highlight = Gtk.StateFlags.INSENSITIVE;
          return;
        }
      }
      
      if (hover)
      {
        box.highlight = Gtk.StateFlags.PRELIGHT;
        return;
      }
    }
    else
    {
      if (hover)
      {
        box.highlight = Gtk.StateFlags.PRELIGHT;
        return;
      }
    }
    
    box.highlight = Gtk.StateFlags.NORMAL;
  }

  void handle_release(CharBox box, bool digit_box)
  {
    if (!box.visible) // in drag
      return;
    
    if (digit_box)
      start_digit_choice (box);
    else
      start_char_choice (box);
  }
  
  void handle_none_release()
  {
    clear_mode();
  }
}

