--  PSL - Optimize NFA
--  Copyright (C) 2002-2016 Tristan Gingold
--
--  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 2 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 <gnu.org/licenses>.

with Types; use Types;
with PSL.Types; use PSL.Types;
with PSL.NFAs.Utils; use PSL.NFAs.Utils;
with PSL.CSE;

package body PSL.Optimize is
   procedure Push (Head : in out NFA_State; S : NFA_State) is
   begin
      Set_State_User_Link (S, Head);
      Head := S;
   end Push;

   procedure Pop (Head : in out NFA_State; S : out NFA_State) is
   begin
      S := Head;
      Head := Get_State_User_Link (S);
   end Pop;

   procedure Remove_Unreachable_States (N : NFA)
   is
      Start : constant NFA_State := Get_Start_State (N);
      Final : constant NFA_State := Get_Final_State (N);
      Active : constant NFA_State := Get_Active_State (N);
      Head : NFA_State;
      E : NFA_Edge;
      S, N_S : NFA_State;
   begin
      --  Remove unreachable states, ie states that can't be reached from
      --  start state.

      Head := No_State;

      --  The start state is reachable.
      Push (Head, Start);
      Set_State_Flag (Start, True);

      --  Follow edges and mark reachable states.
      while Head /= No_State loop
         Pop (Head, S);
         E := Get_First_Src_Edge (S);
         while E /= No_Edge loop
            S := Get_Edge_Dest (E);
            if not Get_State_Flag (S) then
               Push (Head, S);
               Set_State_Flag (S, True);
            end if;
            E := Get_Next_Src_Edge (E);
         end loop;
      end loop;

      --  Remove unreachable states.
      S := Get_First_State (N);
      while S /= No_State loop
         N_S := Get_Next_State (S);
         if Get_State_Flag (S) then
            --  Clean-up.
            Set_State_Flag (S, False);
         elsif S = Final then
            --  Do not remove final state!
            --  FIXME: deconnect state?
            null;
         elsif S = Active then
            --  Do not remove the active state, so that user can see that's
            --  vacuous.
            null;
         else
            Remove_State (N, S);
         end if;
         S := N_S;
      end loop;

      --  Remove no-where states, ie states that can't reach the final state.
      Head := No_State;

      --  The final state can reach the final state.
      Push (Head, Final);
      Set_State_Flag (Final, True);

      --  Follow edges and mark reachable states.
      while Head /= No_State loop
         Pop (Head, S);
         E := Get_First_Dest_Edge (S);
         while E /= No_Edge loop
            S := Get_Edge_Src (E);
            if not Get_State_Flag (S) then
               Push (Head, S);
               Set_State_Flag (S, True);
            end if;
            E := Get_Next_Dest_Edge (E);
         end loop;
      end loop;

      --  Remove unreachable states.
      S := Get_First_State (N);
      while S /= No_State loop
         N_S := Get_Next_State (S);
         if Get_State_Flag (S) then
            --  Clean-up.
            Set_State_Flag (S, False);
         elsif S = Start then
            --  Do not remove start state!
            --  FIXME: deconnect state?
            null;
         elsif S = Active then
            --  The active state is not expected to be reach the final state.
            null;
         else
            Remove_State (N, S);
         end if;
         S := N_S;
      end loop;
   end Remove_Unreachable_States;

   function Has_Loop_On_Start (N : NFA) return Boolean
   is
      Start : constant NFA_State := Get_Start_State (N);
      E : NFA_Edge;
   begin
      E := Get_First_Src_Edge (Start);
      while E /= No_Edge loop
         if Get_Edge_Dest (E) = Start
           and then Get_Edge_Expr (E) = True_Node
         then
            return True;
         end if;
         E := Get_Next_Src_Edge (E);
      end loop;
      return False;
   end Has_Loop_On_Start;

   procedure Remove_Simple_Prefix (N : NFA)
   is
      Start : NFA_State;
      D : NFA_State;
      T_Start, T_D, Next_T_D : NFA_Edge;
      T_Expr : Node;
      Clean : Boolean := False;
   begin
      Start := Get_Start_State (N);

      --  Iterate on edges from the start state.
      T_Start := Get_First_Src_Edge (Start);
      while T_Start /= No_Edge loop
         --  Edge destination.
         D := Get_Edge_Dest (T_Start);
         T_Expr := Get_Edge_Expr (T_Start);

         --  Iterate on destination incoming edges.
         T_D := Get_First_Dest_Edge (D);
         while T_D /= No_Edge loop
            Next_T_D := Get_Next_Dest_Edge (T_D);
            --  Remove parallel edge.
            if T_D /= T_Start
              and then Get_Edge_Expr (T_D) = T_Expr
            then
               Remove_Edge (T_D);
               Clean := True;
            end if;
            T_D := Next_T_D;
         end loop;
         T_Start := Get_Next_Src_Edge (T_Start);
      end loop;
      if Clean then
         Remove_Unreachable_States (N);
      end if;
   end Remove_Simple_Prefix;

   --  Return TRUE iff the outgoing or incoming edges of L and R are the same.
   --  Outgoing edges must be sorted.
   generic
      with function Get_First_Edge (S : NFA_State) return NFA_Edge;
      with function Get_Next_Edge (E : NFA_Edge) return NFA_Edge;
      with function Get_Edge_State_Reverse (E : NFA_Edge) return NFA_State;
   function Are_States_Identical_Gen (L, R : NFA_State) return Boolean;

   function Are_States_Identical_Gen (L, R : NFA_State) return Boolean
   is
      L_E, R_E : NFA_Edge;
      L_S, R_S : NFA_State;
   begin
      L_E := Get_First_Edge (L);
      R_E := Get_First_Edge (R);
      loop
         if L_E = No_Edge and then R_E = No_Edge then
            --  End of chain for both L and R -> identical states.
            return True;
         elsif L_E = No_Edge or R_E = No_Edge then
            --  End of chain for either L or R -> non identical states.
            return False;
         elsif Get_Edge_Expr (L_E) /= Get_Edge_Expr (R_E) then
            --  Different edge (different expressions).
            return False;
         end if;
         L_S := Get_Edge_State_Reverse (L_E);
         R_S := Get_Edge_State_Reverse (R_E);
         if L_S /= R_S and then not (L_S = L and then R_S = R) then
            --  Predecessors are differents and not loop.
            return False;
         end if;
         L_E := Get_Next_Edge (L_E);
         R_E := Get_Next_Edge (R_E);
      end loop;
   end Are_States_Identical_Gen;

   generic
      with procedure Sort_Edges (N : NFA);
      with procedure Sort_Edges_Reverse (S : NFA_State);
      with function Get_First_Edge (S : NFA_State) return NFA_Edge;
      with function Get_Next_Edge (E : NFA_Edge) return NFA_Edge;
      with function Get_First_Edge_Reverse (S : NFA_State) return NFA_Edge;
      with function Get_Next_Edge_Reverse (E : NFA_Edge) return NFA_Edge;
      with function Get_Edge_State (E : NFA_Edge) return NFA_State;
      with function Get_Edge_State_Reverse (E : NFA_Edge) return NFA_State;
      with procedure Merge_State_Reverse (N : NFA;
                                          S : NFA_State; S1 : NFA_State);
   procedure Merge_Identical_States_Gen (N : NFA);

   procedure Merge_Identical_States_Gen (N : NFA)
   is
      function Are_States_Identical is new Are_States_Identical_Gen
        (Get_First_Edge => Get_First_Edge,
         Get_Next_Edge => Get_Next_Edge,
         Get_Edge_State_Reverse => Get_Edge_State_Reverse);

      S, Next_S : NFA_State;
      E : NFA_Edge;
      E_State, Next_E_State : NFA_State;
      Next_E, Next_Next_E : NFA_Edge;
   begin
      Sort_Edges (N);

      --  Iterate on states.
      S := Get_First_State (N);
      while S /= No_State loop
         Next_S := Get_Next_State (S);

         --  Sort edges by expression, so that it is easier to check for
         --  equality.
         Sort_Edges_Reverse (S);

         --  Iterate on incoming (resp. outgoing) edges.
         E := Get_First_Edge_Reverse (S);
         L_Edge: while E /= No_Edge loop
            --  Get the source (resp. dest) state of the edge.
            E_State := Get_Edge_State (E);

            --  Try to merge E with its successors.
            Next_E := Get_Next_Edge_Reverse (E);
            while Next_E /= No_Edge
              and then Get_Edge_Expr (E) = Get_Edge_Expr (Next_E)
            loop
               Next_E_State := Get_Edge_State (Next_E);
               Next_Next_E := Get_Next_Edge_Reverse (Next_E);
               if Next_E_State = E_State then
                  --  Identical edge (same edge expression, same states):
                  --  remove the duplicate.
                  Remove_Edge (Next_E);
               elsif Are_States_Identical (E_State, Next_E_State) then
                  --  Two edges (E and Next_E) have the same expressions and
                  --  coming from (resp. going to) two equivalent states.
                  Merge_State_Reverse (N, E_State, Next_E_State);

                  --  If the current state has been merged, it doesn't exist
                  --  anymore.  Go to the next state.
                  exit L_Edge when Next_E_State = S;
               end if;
               Next_E := Next_Next_E;
            end loop;

            E := Get_Next_Edge_Reverse (E);
         end loop L_Edge;

         S := Next_S;
      end loop;
   end Merge_Identical_States_Gen;

   procedure Merge_Identical_States_Src is new Merge_Identical_States_Gen
     (Sort_Edges => Sort_Src_Edges,
      Sort_Edges_Reverse => Sort_Dest_Edges,
      Get_First_Edge => Get_First_Src_Edge,
      Get_Next_Edge => Get_Next_Src_Edge,
      Get_First_Edge_Reverse => Get_First_Dest_Edge,
      Get_Next_Edge_Reverse => Get_Next_Dest_Edge,
      Get_Edge_State => Get_Edge_Src,
      Get_Edge_State_Reverse => Get_Edge_Dest,
      Merge_State_Reverse => Merge_State_Dest);

   procedure Merge_Identical_States_Dest is new Merge_Identical_States_Gen
     (Sort_Edges => Sort_Dest_Edges,
      Sort_Edges_Reverse => Sort_Src_Edges,
      Get_First_Edge => Get_First_Dest_Edge,
      Get_Next_Edge => Get_Next_Dest_Edge,
      Get_First_Edge_Reverse => Get_First_Src_Edge,
      Get_Next_Edge_Reverse => Get_Next_Src_Edge,
      Get_Edge_State => Get_Edge_Dest,
      Get_Edge_State_Reverse => Get_Edge_Src,
      Merge_State_Reverse => Merge_State_Src);

   procedure Merge_Identical_States (N : NFA) is
   begin
      Merge_Identical_States_Src (N);
      Merge_Identical_States_Dest (N);
   end Merge_Identical_States;

   procedure Merge_Edges (N : NFA)
   is
      use PSL.CSE;
      Nbr_States : Natural;
   begin
      Labelize_States (N, Nbr_States);
      declare
         Last_State : constant Int32 := Int32 (Nbr_States) - 1;
         type Edge_Array is array (0 .. Last_State) of NFA_Edge;
         Edges : Edge_Array;
         S, D : NFA_State;
         L_D : Int32;
         E, Next_E : NFA_Edge;
      begin
         --  Iterate on states.
         S := Get_First_State (N);
         while S /= No_State loop

            Edges := (others => No_Edge);

            --  Iterate on edges whose source is S.
            E := Get_First_Src_Edge (S);
            while E /= No_Edge loop
               Next_E := Get_Next_Src_Edge (E);
               D := Get_Edge_Dest (E);
               L_D := Get_State_Label (D);
               if Edges (L_D) /= No_Edge then
                  --  There is already an edge with the same source and the
                  --  same destination label.
                  Set_Edge_Expr
                    (Edges (L_D),
                     Build_Bool_Or (Get_Edge_Expr (Edges (L_D)),
                                    Get_Edge_Expr (E)));
                  --  FIXME: reduce expression.
                  Remove_Edge (E);
               else
                  Edges (L_D) := E;
               end if;
               E := Next_E;
            end loop;

            S := Get_Next_State (S);
         end loop;
      end;
   end Merge_Edges;

   procedure Remove_Identical_Src_Edges (S : NFA_State)
   is
      Next_E, E : NFA_Edge;
   begin
      Sort_Src_Edges (S);
      E := Get_First_Src_Edge (S);
      if E = No_Edge then
         return;
      end if;
      loop
         Next_E := Get_Next_Src_Edge (E);
         exit when Next_E = No_Edge;
         if Get_Edge_Dest (E) = Get_Edge_Dest (Next_E)
           and then Get_Edge_Expr (E) = Get_Edge_Expr (Next_E)
         then
            Remove_Edge (Next_E);
         else
            E := Next_E;
         end if;
      end loop;
   end Remove_Identical_Src_Edges;

   procedure Remove_Identical_Dest_Edges (S : NFA_State)
   is
      Next_E, E : NFA_Edge;
   begin
      Sort_Dest_Edges (S);
      E := Get_First_Dest_Edge (S);
      if E = No_Edge then
         return;
      end if;
      loop
         Next_E := Get_Next_Dest_Edge (E);
         exit when Next_E = No_Edge;
         if Get_Edge_Src (E) = Get_Edge_Src (Next_E)
           and then Get_Edge_Expr (E) = Get_Edge_Expr (Next_E)
         then
            Remove_Edge (Next_E);
         else
            E := Next_E;
         end if;
      end loop;
   end Remove_Identical_Dest_Edges;

   procedure Find_Partitions (N : NFA; Nbr_States : Natural)
   is
      Last_State : constant NFA_State := NFA_State (Nbr_States) - 1;
      type Part_Offset is new Int32 range -1 .. Nat32 (Nbr_States - 1);
      type Part_Id is new Part_Offset range 0 .. Part_Offset'Last;

      --  State to partition id.
      State_Part : array (0 .. Last_State) of Part_Id;
      pragma Unreferenced (State_Part);

      --  Last partition index.
      Last_Part : Part_Id;

      --  Partitions content.

      --  To get the states in a partition P, first get the offset OFF
      --  (from Offsets) of P.  States are in Parts (OFF ...).  The
      --  number of states is not known, but they all belong to P
      --  (check with STATE_PART).
      Parts : array (Part_Offset) of NFA_State;
      type Offset_Array is array (Part_Id) of Part_Offset;
      Start_Offsets : Offset_Array;
      Last_Offsets : Offset_Array;

      S, Final_State : NFA_State;
      First_S : NFA_State;
      Off, Last_Off : Part_Offset;

      Stable, Stable1 : Boolean;

      function Is_Equivalent (L, R : NFA_State) return Boolean is
      begin
         raise Program_Error;
         return False;
      end Is_Equivalent;
   begin
      --  Return now for trivial cases (0 or 1 state).
      if Nbr_States < 2 then
         return;
      end if;

      --  Partition 1 contains the final state.
      --  Partition 0 contains the other states.
      Final_State := Get_Final_State (N);
      Last_Part := 1;
      State_Part := (others => 0);
      State_Part (Final_State) := 1;
      S := Get_First_State (N);
      Off := -1;
      while S /= No_State loop
         if S /= Last_State then
            Off := Off + 1;
            Parts (Off) := S;
         end if;
         S := Get_Next_State (S);
      end loop;
      Start_Offsets (0) := 0;
      Last_Offsets (0) := Off;
      Start_Offsets (1) := Off + 1;
      Last_Offsets (1) := Off + 1;
      Parts (Off + 1) := Final_State;

      --  Now the hard work.
      loop
         Stable := True;
         --  For every partition
         for P in 0 .. Last_Part loop
            Off := Start_Offsets (P);
            First_S := Parts (Off);
            Off := Off + 1;

            --  For every S != First_S in P.
            Last_Off := Last_Offsets (P);
            Stable1 := True;
            while Off <= Last_Off loop
               S := Parts (Off);

               if not Is_Equivalent (First_S, S) then
                  --  Swap S with the last element of the partition.
                  Parts (Off) := Parts (Last_Off);
                  Parts (Last_Off) := S;
                  --  Reduce partition size.
                  Last_Off := Last_Off - 1;
                  Last_Offsets (P) := Last_Off;

                  if Stable1 then
                     --  Create a new partition.
                     Last_Part := Last_Part + 1;
                     Last_Offsets (Last_Part) := Last_Off + 1;
                     Stable1 := False;
                  end if;
                  --  Put S in the new partition.
                  Start_Offsets (Last_Part) := Last_Off + 1;
                  State_Part (S) := Last_Part;
                  Stable := False;

                  --  And continue with the swapped state.
               else
                  Off := Off + 1;
               end if;
            end loop;
         end loop;
         exit when Stable;
      end loop;
   end Find_Partitions;
   pragma Unreferenced (Find_Partitions);
end PSL.Optimize;
