/* -*- 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 MultPuzzleGuessStatus {
  WRONG, CORRECT, KNOWN, INVALID
}

public enum MultPuzzleChar {
  A = 65, B, C, D, E, F, G, H, I, J, INVALID
}

public class MultPuzzle : Object
{
  /* Properties */
  public int num_x_digits { get; construct; default = 3; }
  public int num_y_digits { get; construct; default = 2; }
  
  public int total_guesses { get; private set; default = 0; }
  public int wrong_guesses { get; private set; default = 0; }
  public int correct_guesses { get; private set; default = 0; }
  public bool is_done { get; private set; default = false; }
  
  /* Signals */
  public signal void guessed(int digit, MultPuzzleChar letter, MultPuzzleGuessStatus status);
  public signal void changed();
  
  /* Constructors */
  public MultPuzzle(int num_x, int num_y) {
    Object(num_x_digits: num_x, num_y_digits: num_y);
  }
  
  /* Methods */
  public string get_multiplicand()
  {
    return x;
  }
  public string get_multiplier()
  {
    return y;
  }
  public string get_answer()
  {
    return z;
  }
  
  public uint get_num_addends() {return addends.length;}
  public string? get_addend(int n)
  {
    if (n < 0 || n >= addends.length)
      return null;
    
    return addends[n];
  }
  
  public MultPuzzleGuessStatus guess(int digit, MultPuzzleChar letter) {
    if (digit < 0 || digit > 9 || letter == MultPuzzleChar.INVALID)
      return MultPuzzleGuessStatus.INVALID;
    
    MultPuzzleGuessStatus rv;
    
    if (symbols[digit] == letter)
    {
      solve_digit (digit);
      
      total_guesses ++;
      correct_guesses ++;
      have_guessed[((int)letter - (int)MultPuzzleChar.A) * 10 + digit] = true;
      
      rv = MultPuzzleGuessStatus.CORRECT;
    }
    else if (symbols[digit] == MultPuzzleChar.INVALID)
    {
      rv = MultPuzzleGuessStatus.KNOWN;
    }
    else
    {
      total_guesses ++;
      wrong_guesses ++;
      have_guessed[((int)letter - (int)MultPuzzleChar.A) * 10 + digit] = true;
      
      rv =  MultPuzzleGuessStatus.WRONG;
    }
    
    guessed (digit, letter, rv);
    
    return rv;
  }
  
  public void solve() {
    for (int i = 0; i < 10; i++)
      solve_digit(i);
  }
  
  public MultPuzzleChar solve_digit(int digit) {
    if (digit < 0 || digit > 9)
      return MultPuzzleChar.INVALID;
    
    if (!this.needed[digit])
      return MultPuzzleChar.INVALID;
    
    replace (ref x, digit);
    replace (ref y, digit);
    replace (ref z, digit);
    
    for (int i = 0; i < addends.length; ++i)
    {
      string addend = addends[i];
      replace (ref addend, digit);
      addends[i] = addend;
    }
    
    var symbol = symbols[digit];
    symbols[digit] = MultPuzzleChar.INVALID;
    needed[digit] = false;
    unknown[digit] = false;
    
    bool any_left = false;
    foreach (bool g in needed)
      if (g)
      {
        any_left = true;
        break;
      }
    if (!any_left)
      is_done = true;
    
    changed ();
    return symbol;
  }
  
  public bool[] get_unknown_digits()
  {
    bool[] rv = new bool[unknown.length];
    for (int i = 0; i < unknown.length; i++)
      rv[i] = unknown[i];
    return rv;
  }
  
  public bool[] get_needed_digits()
  {
    bool[] rv = new bool[needed.length];
    for (int i = 0; i < needed.length; i++)
      rv[i] = needed[i];
    return rv;
  }
  
  public bool[] get_letter_guesses(MultPuzzleChar letter)
  {
    bool[] rv = new bool[10];
    
    if (letter == MultPuzzleChar.INVALID)
      for (int i = 0; i < 10; i++)
        rv[i] = false;
    else
      for (int i = 0; i < 10; i++)
        rv[i] = have_guessed[((int)letter - (int)MultPuzzleChar.A) * 10 + i];
    
    return rv;
  }
  
  /* Private methods */
  
  // pads a string with 0's in the front if it is less than the minimum
  private void expand_num_string (ref string n, size_t minimum) {
    if (n.length >= minimum)
      return;
    
    size_t difference = minimum - n.length;
    
    StringBuilder rv = new StringBuilder ();
    
    for (size_t i = 0; i < difference; ++i)
      rv.append_c ('0');
    rv.append (n);
    
    n = rv.str;
  }
  
  private static void shuffle(MultPuzzleChar[] symbolList) {
    /**
     * Here, we need to pick a spot from 0...9, and place the first digit there.
     * Then, we pick a spot k from 0...8, and place the next digit in the kth free spot.
     * Repeat.
     */
    
    for (int i = 0; i < 10; i++) {
      int spot = Random.int_range(0, 10 - i);
      int j;
      
      for (j = 0; j < 10; j++) {
        if (symbolList[j] == MultPuzzleChar.INVALID)  {
          if (spot-- == 0)
            break;
        }
      }
      
      symbolList[j] = (MultPuzzleChar)(i + 65);
    }
  }
  
  private void replace(ref string n, int digit)
  {
    StringBuilder rv = new StringBuilder ();
    
    weak string np = n;
    while (np.length > 0)
    {
      unichar ch = np.get_char ();
      if ((int)ch == (int)symbols[digit])
        rv.append_c ((char)(digit + 48));
      else
        rv.append_unichar (ch);
      np = np.next_char ();
    }
    
    n = rv.str;
  }
  
  /* Modifies a list of characters, replacing each that represents an unguessed
     incoming integer with the appropriate letter. */
  private void interpret(ref string n)
  {
    StringBuilder rv = new StringBuilder ();
    
    weak string np = n;
    while (np.length > 0)
    {
      unichar ch = np.get_char ();
      if (ch.isdigit ())
        rv.append_c ((char)symbols[ch.digit_value ()]);
      else
        rv.append_unichar (ch);
      np = np.next_char ();
    }
    
    n = rv.str;
  }
  
  /* n is  a string full of digit characters */
  private void note_needed(string n)
  {
    weak string np = n;
    while (np.length > 0)
    {
      unichar ch = np.get_char ();
      if (ch.isdigit ())
        needed[ch.digit_value ()] = true;
      np = np.next_char ();
    }
  }
  
  private static int num_with_n_digits(int n)
  {
    int32 low, high;
    
    if (n <= 1)
      low = 0;
    else
      low = (int32) Math.pow (10, n - 1);
    
    high = (int32) Math.pow (10, n);
    
    return Random.int_range(low, high);
  }
  
  /* Private members */
  private MultPuzzleChar[] symbols; // the character for each digit 0-9

  /* if a digit has yet to be guessed, its value is true
     else, if a digit has been guessed, or does not appear on the board, 
     its value is false */
  private bool[] needed;

  // if a digit has yet to be guessed, its value is true, else false 
  private bool[] unknown;

  // if a guess has been made for a digit, the value for the digit will be true 
  // rows are letters, columns are digits 
  private bool[] have_guessed;
  
  // X is the multiplicand, Y the multiplier, and Z the answer.
  private string x;
  private string y;
  private string z;
  
  // This is a list of the addends resulting from the list of X and Y.
  private string[] addends;
  
  construct
  {
    needed = new bool[10];
    unknown = new bool[10];
    symbols = new MultPuzzleChar[10];
    have_guessed = new bool[100];
    
    for (int i = 0; i < 10; ++i)
    {
      this.needed[i] = false;
      this.unknown[i] = true;
      this.symbols[i] = MultPuzzleChar.INVALID;
      
      for (int j = 0; j < 10; j++)
        have_guessed[i * 10 + j] = false;
    }
    
    shuffle (symbols);
    
    // Get value for x and y.
    int valX = num_with_n_digits (num_x_digits);
    int valY;
    do {
      valY = num_with_n_digits (num_y_digits);
    } while (valY % 10 == 0);
    int valZ = valX * valY;
    
    x = valX.to_string ();
    y = valY.to_string ();
    z = valZ.to_string ();
    expand_num_string (ref z, x.length + y.length);
    
    // Calculate addends.
    addends = new string[y.length];
    
    for (int i = 0; i < y.length; i++)
    {
      int addend = valX * (y[y.length - i - 1].digit_value());
      
      string addend_str = addend.to_string ();
      
      expand_num_string (ref addend_str, x.length + 1);
      note_needed (addend_str);
      interpret (ref addend_str);
      
      addends[i] = addend_str;
    }
    
    note_needed (x);
    note_needed (y);
    note_needed (z);
    
    interpret (ref x);
    interpret (ref y);
    interpret (ref z);
  }
}

