/* -*- Mode: Vala; indent-tabs-mode: nil; c-basic-offset: 2; tab-width: 2 -*- */
/*
    Multiplication Puzzle
    Copyright (C) 2008,2011 Michael Terry <mike@mterry.name>

    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 3 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/>.
*/

using GLib;

public enum CanvasMode {NONE, DIGIT, CHAR}

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

public class Canvas : Gtk.EventBox
{
  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);
    
    var context_id = mult.status.get_context_id ("guess-prompt");
    mult.status.pop(context_id); // There might be an existing prompt
    if (mode != CanvasMode.NONE) {
      mult.clear_guess_feedback();
      mult.status.push (context_id, "%s = ?".printf (_(mode_letter)));
    }
  }
  
  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 min_box_request;
  
  private int num_box_rows;
  private int num_box_cols; // Not counting two digit cols
  
  private int min_width;
  private int min_height;
  
  private Gtk.Fixed fixed;
  private Gtk.Widget vsep;
  private Gtk.Widget hsep1;
  private Gtk.Widget hsep2;
  
  private List<CharBox> mult_boxes;
  private List<CharBox> digit_boxes;
  
  private CharBox[] xboxes;
  private CharBox[] yboxes;
  private CharBox[] zboxes;
  private ArrayBox[] addboxes;
  private CharBox[] digitboxes;
  private CharBox plusbox;
  private CharBox timesbox;
  
  // Standard buffer between widgets
  private static const int SPACING = 6;
  
  construct
  {
    this.puzzle = mult.puzzle;
    this.mode = CanvasMode.NONE;
    this.mode_letter = null;
    this.min_box_request = -1;
    this.mult_boxes = null;
    this.digit_boxes = null;
    
    this.fixed = new Gtk.Fixed();
    add(this.fixed);
    
    this.vsep = new Gtk.VSeparator();
    this.hsep1 = new Gtk.HSeparator();
    this.hsep2 = new Gtk.HSeparator();
    this.fixed.add(this.vsep);
    this.fixed.add(this.hsep1);
    this.fixed.add(this.hsep2);
    
    this.plusbox = new CharBox("+", Gtk.ShadowType.NONE);
    this.fixed.add(this.plusbox);
    
    this.timesbox = new CharBox("×", Gtk.ShadowType.NONE);
    this.fixed.add(this.timesbox);
    
    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 CharBox[x.length];
    this.yboxes = new CharBox[y.length];
    this.zboxes = new CharBox[z.length];
    this.digitboxes = new CharBox[10];
    
    this.addboxes = new ArrayBox[this.puzzle.get_num_addends()];
    for (int i = 0; i < this.addboxes.length; ++i)
      this.addboxes[i].array = new CharBox[this.puzzle.get_addend(i).length];
    
    // Calculate minimum amount of space needed.
    int vsep_width, hsep1_height;
    // FIXME: vala bindings are broken, we'll just fake preferred width
    //vsep.get_preferred_width(null, out vsep_width);
    hsep1.get_preferred_height(null, out hsep1_height);
    vsep_width = hsep1_height;
    this.min_width = 12 + (this.num_box_cols-1)*SPACING + SPACING + vsep_width + SPACING*4 + 12;
    this.min_height = 12 + (this.num_box_rows+1)*SPACING + hsep1_height*2 + 12;
    set_size_request(min_width, min_height);
    
    /*
    Gtk.TargetEntry[] entries = new Gtk.TargetEntry[1];
    entries[0].target = "STRING";
    entries[0].flags = Gtk.TargetFlags.SAME_APP;
    entries[0].info = 0;
    Gtk.drag_dest_set (this, 0, entries, Gdk.DragAction.MOVE);
    */
    
    add_events (Gdk.EventMask.BUTTON_RELEASE_MASK);
    button_release_event.connect(handle_none_release);
    
    this.fixed.size_allocate.connect(handle_size_allocate);
    
    handle_puzzle_change(this.puzzle);
    this.puzzle.changed.connect(handle_puzzle_change);
  }
  
  private void layout_mult_string(CharBox[] boxes, int row, int indent, int w, int mult_x_offset, int mult_y_offset)
  {
    indent = this.num_box_cols - indent;
    int x = mult_x_offset + indent*(w+SPACING);
    int y = mult_y_offset + row*(w+SPACING);
    
    if (row >= 2) // addend
      y += this.hsep1.get_allocated_height() + SPACING;
    if (row == this.num_box_rows - 1) // answer
      y += this.hsep2.get_allocated_height() + SPACING;
    
    foreach (CharBox b in boxes)
    {
      Gdk.Rectangle alloc = {x, y, w, w};
      allocate_box(b, alloc);
      
      x += w+SPACING;
    }
  }
  
  private void allocate_box(CharBox b, Gdk.Rectangle rect)
  {
    b.size_allocate(rect);
    b.points = (int) ((rect.height) * (72.0/GtkMult.dpi));
  }
  
  private void handle_size_allocate(Gdk.Rectangle rect)
  {
    int w = (int) ((rect.width - this.min_width) / (this.num_box_cols + 2));
    int h = (int) ((rect.height - this.min_height) / this.num_box_rows);
    
    // From here on, we only use 'w' because we want a square box
    w = (w > h) ? h : w;
    
    int mult_x_offset = rect.x + 12 + (rect.width - this.min_width - w*(this.num_box_cols+2))/2;
    int mult_y_offset = rect.y + 12 + (rect.height - this.min_height - w*this.num_box_rows)/2;
    mult_x_offset = mult_x_offset > (rect.width/2) ? (rect.width/2) : mult_x_offset;
    mult_y_offset = mult_y_offset > (rect.height/2) ? (rect.height/2) : mult_y_offset;
    
    layout_mult_string(this.xboxes, 0, this.xboxes.length, w, mult_x_offset, mult_y_offset);
    layout_mult_string(this.yboxes, 1, this.yboxes.length, w, mult_x_offset, mult_y_offset);
    for (int i = 0; i < this.addboxes.length; ++i)
      layout_mult_string(this.addboxes[i].array, 2 + i, this.addboxes[i].array.length + i, w, mult_x_offset, mult_y_offset);
    layout_mult_string(this.zboxes, this.num_box_rows - 1, this.zboxes.length, w, mult_x_offset, mult_y_offset);
    
    // *** Horizontal separators ***
    int hsep1_height, hsep2_height;
    hsep1.get_preferred_height(null, out hsep1_height);
    hsep2.get_preferred_height(null, out hsep2_height);

    Gdk.Rectangle rec = Gdk.Rectangle();
    rec.x = mult_x_offset;
    rec.y = mult_y_offset + 2*(w+SPACING);
    rec.width = this.num_box_cols*w + (this.num_box_cols-1)*SPACING;
    rec.height = hsep1_height;
    this.hsep1.size_allocate(rec);
    
    rec.x = mult_x_offset;
    rec.y = mult_y_offset + (int)(2+this.puzzle.get_num_addends())*(w+SPACING) + hsep1_height + SPACING;
    rec.width = this.num_box_cols*w + (this.num_box_cols-1)*SPACING;
    rec.height = hsep2_height;
    this.hsep2.size_allocate(rec);
    
    // *** Symbols ****
    rec.x = mult_x_offset;
    rec.y = mult_y_offset + w + SPACING;
    rec.width = w;
    rec.height = w;
    allocate_box(this.timesbox, rec);
    
    rec.x = mult_x_offset;
    rec.y = mult_y_offset + (int)(1+this.puzzle.get_num_addends())*(w+SPACING) + hsep1_height + SPACING;
    rec.width = w;
    rec.height = w;
    allocate_box(this.plusbox, rec);
    
    // *** Vertical separator ***
    int vsep_width;
    // FIXME: vala bindings are broken, we'll just fake preferred width
    //vsep.get_preferred_width(null, out vsep_width);
    vsep_width = hsep1_height;

    rec.x = mult_x_offset + this.num_box_cols*(w+SPACING) + SPACING;
    rec.y = mult_y_offset;
    rec.width = vsep_width;
    rec.height = rect.height - (mult_y_offset-rect.y)*2;
    this.vsep.size_allocate(rec);
    
    // *** Digits ***
    int digit_y_offset = rect.y + 12 + (rect.height - 12 - 12 - 5*w - 4*SPACING)/2;
    rec.width = w;
    rec.height = w;
    
    rec.x = rec.x + SPACING*2;
    for (int i = 0; i < 5; ++i) {
      rec.y = digit_y_offset + i*(w+SPACING);
      allocate_box(this.digitboxes[i*2], rec);
    }
    
    rec.x = rec.x + w + SPACING;
    for (int i = 0; i < 5; ++i) {
      rec.y = digit_y_offset + i*(w+SPACING);
      allocate_box(this.digitboxes[i*2 + 1], rec);
    }
  }
  
  private CharBox create_mult_box(unichar ch)
  {
    CharBox b;
    if (ch.isdigit ())
      b = new CharBox("%c".printf((char)ch), Gtk.ShadowType.NONE);
    else {
      b = new TableBox((char)ch, this);
    
      b.add_events (Gdk.EventMask.BUTTON_RELEASE_MASK |
                    Gdk.EventMask.ENTER_NOTIFY_MASK |
                    Gdk.EventMask.LEAVE_NOTIFY_MASK);
      
      b.button_release_event.connect((e) => {
        return handle_release (b, e, false);});
      b.enter_notify_event.connect((e) => {
        highlight_box (b, false, true, true);
        return false;});
      b.leave_notify_event.connect((e) => {
        highlight_box (b, false, false, true);
        return false;});
      
      mult_boxes.append(b);
    }
    
    return b;
  }
  
  private CharBox create_digit_box(bool[] unknown, int i)
  {
    CharBox b;
    if (unknown[i] == true) {
      var digit_str = "%i".printf(i);
      b = new DigitBox(this, digit_str);
      
      b.add_events (Gdk.EventMask.BUTTON_RELEASE_MASK |
                    Gdk.EventMask.ENTER_NOTIFY_MASK |
                    Gdk.EventMask.LEAVE_NOTIFY_MASK);
      
      b.button_release_event.connect((e) => {
        return handle_release (b, e, true);});
      b.enter_notify_event.connect((e) => {
        highlight_box (b, true, true, true);
        return false;});
      b.leave_notify_event.connect((e) => {
        highlight_box (b, true, false, true);
        return false;});
      
      digit_boxes.append(b);
    }
    else
      b = new CharBox (" ", Gtk.ShadowType.NONE);
    
    return b;
  }
  
  private void handle_puzzle_change(MultPuzzle p)
  {
    // Create CharBoxes for all the pieces of the puzzle.
    
    this.mult_boxes = new List<CharBox>();
    this.digit_boxes = new List<CharBox>();
    
    var multiplicand = this.puzzle.get_multiplicand();
    for (int i = 0; i < multiplicand.length; ++i) {
      this.xboxes[i] = create_mult_box(multiplicand[i]);
      this.xboxes[i].show_all();
      this.fixed.add(this.xboxes[i]);
    }
    
    var multiplier = this.puzzle.get_multiplier();
    for (int i = 0; i < multiplier.length; ++i) {
      this.yboxes[i] = create_mult_box(multiplier[i]);
      this.yboxes[i].show_all();
      this.fixed.add(this.yboxes[i]);
    }
    
    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]);
        this.addboxes[i].array[j].show_all();
        this.fixed.add(this.addboxes[i].array[j]);
      }
    }
    
    var answer = this.puzzle.get_answer();
    for (int i = 0; i < answer.length; ++i) {
      this.zboxes[i] = create_mult_box(answer[i]);
      this.zboxes[i].show_all();
      this.fixed.add(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.digitboxes[i].show_all();
      this.fixed.add(this.digitboxes[i]);
    }
    
    if (p.is_done) {
      this.sensitive = false;
      
      for (int i = 0; i < 10; i++)
        this.digitboxes[i].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;
  }
  
  private bool handle_release (CharBox box, Gdk.EventButton event, bool digit_box)
  {
    if (!box.visible) // in drag
      return false;
    
    // Make sure pointer is still in our box.  This ignores trying to grab
    // when the widget doesn't support it.
    int x, y, w, h;
    box.get_pointer (out x, out y);
    w = get_window().get_width();
    h = get_window().get_height();
    if (x.clamp(0, w) != x || y.clamp(0, h) != y)
      return false;
    
    if (event.button == 1)
    {
      if (digit_box)
        start_digit_choice (box);
      else
        start_char_choice (box);
      return true;
    }
    return false;
  }
  
  private bool handle_none_release (Gdk.EventButton event)
  {
    // Make sure pointer is still in our box.  This ignores trying to grab
    // when the widget doesn't support it.
    int x, y, w, h;
    get_pointer (out x, out y);
    w = get_window().get_width();
    h = get_window().get_height();
    if (x.clamp(0, w) != x || y.clamp(0, h) != y)
      return false;
    
    if (event.button == 1)
      clear_mode ();
    return true;
  }
}

