--  Match_3: library for managing the logics (not the graphics) of a "Match-3"-style game.

--  Legal licensing note:

--  Copyright (c) 2017 Gautier de Montmollin

--  Permission is hereby granted, free of charge, to any person obtaining a copy
--  of this software and associated documentation files (the "Software"), to deal
--  in the Software without restriction, including without limitation the rights
--  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
--  copies of the Software, and to permit persons to whom the Software is
--  furnished to do so, subject to the following conditions:

--  The above copyright notice and this permission notice shall be included in
--  all copies or substantial portions of the Software.

--  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
--  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
--  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
--  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
--  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
--  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
--  THE SOFTWARE.

--  NB: this is the MIT License, as found 12-Sep-2007 on the site
--  http://www.opensource.org/licenses/mit-license.php

with Ada.Numerics.Float_Random; use Ada.Numerics.Float_Random;

package body Match_3 is

   function Find_any_match
     (b : Board)
      return Boolean
   is
   begin
      for x in b'Range (1) loop
         for y in b'Range (2) loop
            if b (x, y) in Movable_tile then
               --  Search right
               for x2 in x + 1 .. b'Last (1) loop
                  exit when b (x2, y) /= b (x, y);
                  if x2 = x + 2 then
                     return True;
                  end if;
               end loop;
               --  Search below
               for y2 in y + 1 .. b'Last (2) loop
                  exit when b (x, y2) /= b (x, y);
                  if y2 = y + 2 then
                     return True;
                  end if;
               end loop;
            end if;
         end loop;
      end loop;
      return False;
   end Find_any_match;

   function Random_cell
     (gen : Generator;
      p   : Tile_prob)
      return Cell
   is
      v, vt : Prob;
      res   : Cell;
   begin
      v  := Prob (Random (gen));
      vt := 0.0;
      --  This is for having b (x, y) set in the 0-prob. case where v = 1.
      res := Simple_tile'Last;
      for t in Simple_tile loop
         vt := vt + p (t);  --  Cumulative probability
         if v < vt then
            res := t;
            exit;
         end if;
      end loop;
      return res;
   end Random_cell;

   procedure Fill
     (b : in out Board;
      p :        Tile_prob)
   is
      gen : Generator;
   begin
      Reset (gen);
      loop
         for x in b'Range (1) loop
            for y in b'Range (2) loop
               b (x, y) := Random_cell (gen, p);
            end loop;
         end loop;
         exit when not Find_any_match (b);
      end loop;
   end Fill;

   function Matches
     (b : Board)
      return Match_list
   is
      ma    : Match_list (1 .. b'Length (1) * b'Length (2));
      nm    : Match_kind;
      n     : Natural := 0;
      found : Boolean;
      --  Variable 'seen' is to prevent multiple-searching [[and multiple-counting matches]]
      seen : array (b'Range (1), b'Range (2)) of Boolean := (others => (others => False));
   begin
      for x in b'Range (1) loop
         for y in b'Range (2) loop
            if b (x, y) in Movable_tile and not seen (x, y) then
               found := False;
               --  Search right
               for x2 in x + 1 .. b'Last (1) loop
                  exit when b (x2, y) /= b (x, y) or seen (x2, y);
                  if x2 = x + 2 then
                     seen (x + 1, y) := True;
                     seen (x2, y)    := True;
                     found           := True;
                     nm              := Hor_3;
                  elsif x2 = x + 3 then
                     seen (x2, y) := True;
                     nm           := Hor_4;
                  elsif x2 >= x + 4 then
                     seen (x2, y) := True;
                     nm           := Hor_5p;
                  end if;
               end loop;
               if not found then
                  --  Search below
                  for y2 in y + 1 .. b'Last (2) loop
                     exit when b (x, y2) /= b (x, y) or seen (x, y2);
                     if y2 = y + 2 then
                        seen (x, y + 1) := True;
                        seen (x, y2)    := True;
                        found           := True;
                        nm              := Ver_3;
                     elsif y2 = y + 3 then
                        seen (x, y2) := True;
                        nm           := Ver_4;
                     elsif y2 >= y + 4 then
                        seen (x, y2) := True;
                        nm           := Ver_5p;
                     end if;
                  end loop;
               end if;
               if found then
                  n      := n + 1;
                  ma (n) := (nm, (x, y));
               end if;
            end if;
         end loop;
      end loop;
      return ma (1 .. n);
   end Matches;

   procedure Empty_matching_cells
     (b  : in out Board;
      ma :        Match_list)
   is
      x, y : Integer;
      c    : Cell;
   begin
      for n in ma'Range loop
         case ma (n).kind is
            when Hor_3 .. Hor_5p =>
               y := ma (n).pos.y;
               c := b (ma (n).pos.x, y);
               for x in ma (n).pos.x .. b'Last (1) loop
                  exit when b (x, y) /= c;
                  b (x, y) := empty;
               end loop;
            when Ver_3 .. Ver_5p =>
               x := ma (n).pos.x;
               c := b (x, ma (n).pos.y);
               for y in ma (n).pos.y .. b'Last (2) loop
                  exit when b (x, y) /= c;
                  b (x, y) := empty;
               end loop;
         end case;
      end loop;
   end Empty_matching_cells;

   function Gravity_step
     (b : Board;
      p : Tile_prob)
      return Gravity_list
   is
      gl              : Gravity_list (1 .. b'Length (1) * (b'Length (2)**2));
      n               : Natural := 0;
      gen             : Generator;
      bsim            : Board   := b;
      found_in_column : Boolean;
      --
      procedure Single_move (gi : Gravity_item) is
      begin
         n      := n + 1;
         gl (n) := gi;
         if gi.from.y > 0 then
            bsim (gi.from.x, gi.from.y) := empty;
         end if;
         bsim (gi.to.x, gi.to.y) := gi.new_content;
      end Single_move;
      --
      --  Position of the new tiles coming above board; there can be a whole column of them!...
      new_tile_y : array (b'Range (1)) of Integer := (others => 0);
      --
      procedure New_tile (x, y : Positive) is
      begin
         Single_move ((new_content => Random_cell (gen, p), from => (x, new_tile_y (x)), to => (x, y)));
         new_tile_y (x) := new_tile_y (x) - 1;
      end New_tile;
   begin
      Reset (gen);
      --  Rows below top row
      for y in reverse 2 .. b'Last (2) loop
         for x in b'Range (1) loop
            if bsim (x, y) = empty then
               found_in_column := False;
               --  Search in the column above
               for yy in reverse 1 .. y - 1 loop
                  case bsim (x, yy) is
                     when empty =>
                        if yy = 1 then  --  whole column above (x,y) is empty, get 1 new tile
                           New_tile (x, y);
                           found_in_column := True;
                           exit;
                        else
                           null;  --  Continue searching
                        end if;
                     when Movable_tile =>
                        Single_move ((new_content => bsim (x, yy), from => (x, yy), to => (x, y)));
                        found_in_column := True;
                        exit;
                     when others =>  --  Any non-movable tile
                        exit;
                  end case;
               end loop;
               if found_in_column then
                  null;  --  Found a movable tile above, or the sky (then, a new random tile)
               elsif x > 1 and then bsim (x - 1, y - 1) in Movable_tile then  --  Diagonal: above, left
                  Single_move ((new_content => bsim (x - 1, y - 1), from => (x - 1, y - 1), to => (x, y)));
               elsif x < b'Last (1) and then bsim (x + 1, y - 1) in Movable_tile then  --  Diagonal: above, right
                  Single_move ((new_content => bsim (x + 1, y - 1), from => (x + 1, y - 1), to => (x, y)));
               end if;
            end if;
         end loop;
      end loop;
      --  Top row
      for x in b'Range (1) loop
         if bsim (x, 1) = empty then
            New_tile (x, 1);
         end if;
      end loop;
      return gl (1 .. n);
   end Gravity_step;

   procedure Apply_gravity_moves
     (b  : in out Board;
      gl :        Gravity_list)
   is
   begin
      for g of gl loop
         if g.from.y > 0 then
            b (g.from.x, g.from.y) := empty;
         end if;
         b (g.to.x, g.to.y) := g.new_content;
      end loop;
   end Apply_gravity_moves;

end Match_3;
