%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%% 
%% Copyright Ericsson AB 2009-2025. All Rights Reserved.
%% 
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%% 
%% %CopyrightEnd%

-module(sudoku_game).

-export([init/1, loop/1,
         indx/1, rcm/1, level/1]).
-include("sudoku.hrl").

init(GFX) ->
    Empty = empty_table(#s{}),
    Add = fun({Butt,Val},SN) ->
		  validate(rcm(Butt),Val,false,SN)
 	  end,

    Game = test(),
    GFX ! {init, Game},
    Self = self(),
    Gen = spawn_opt(fun() -> create_games(levels(),Self) end, 
		    [link, {priority,low}]),
    loop(lists:foldl(Add,Empty#s{gfx=GFX, gen=Gen},Game)).

%%%%%%%%%%%%%%%%%% Game Engine %%%%%%%%%%%%%%%%%%

empty_table(S) ->
    Nine = lists:seq(1,9),
    D    = gb_sets:from_ordset(Nine),
    Mat  = list_to_tuple([D || _ <- Nine]),    
    Poss = list_to_tuple([D || _ <- lists:seq(1,9*9)]),
    Vals = list_to_tuple([0 || _ <- lists:seq(1,9*9)]),
    Must = list_to_tuple([gb_sets:empty() || _ <- lists:seq(1,9*3)]),
    S#s{p=Poss,m=Mat,mr=Must,mc=Must,v=Vals}.

loop(S0 = #s{gfx = Gfx, v=Vs}) ->
    receive 
	quit -> 
	    halt;
	{'EXIT', Gfx, Reason} ->
	    io:format("The GUI crashed: ~p~n", [Reason]);
	{validate, Butt, Val} ->
	    Ix = indx(Butt),
	    case element(Ix,Vs) of
		Val -> loop(S0);
		0 -> 
		    S = validate(rcm(Butt),Val,true,S0),
		    loop(S);
		_ ->
		    S1 = S0#s{v=setelement(Ix,Vs,0)},
		    S2 = rebuild_all(rcm(Butt),S1),
		    S  = validate(rcm(Butt),Val,true,S2),
		    loop(S)
	    end;
	{loaded, Game} ->
	    S1  = empty_table(S0),
	    Add = fun({Butt,Val},SN) ->
			  validate(rcm(Butt),Val,true,SN)
		  end,
	    loop(lists:foldl(Add,S1,Game));
	{op,?EMPTY} ->
	    loop(empty_table(S0));
	{op,?NEW, Level} ->
	    case find_game(Level,S0) of
		{Game, S1} ->
		    S0#s.gen ! {gen_game, Level},
		    Gfx ! {busy,start},
		    Gfx ! {init, Game};
		false -> 
		    S1 = S0,
		    Gfx ! {busy,start},
		    Temp = new_game(S0),
		    Game = pick_shown(Temp,Level,Gfx),
		    S0#s.gen ! {gen_game, Level},
		    Game
	    end,
	    S2  = empty_table(S1),
	    Add = fun({Butt,Val},SN) ->
			  validate(rcm(Butt),Val,false, SN)
		  end,
	    Gfx ! {init, Game},
	    Gfx ! {busy,stop},
	    loop(lists:foldl(Add,S2,Game));
	{solve, All} ->
	    Res =  solve(S0, All),
	    [Gfx ! {set_val, Ind, Val} || {Ind,Val} <- element(2,Res)],
	    loop(S0);
	{get_game, Pid} ->	    
	    Pid ! {game, get_known(S0)},
	    loop(S0);
	{game, Game} ->
	    loop(S0#s{games=[Game|S0#s.games]});
	CMD ->
	    io:format("Game loop got ~p~n", [CMD]),
	    ?MODULE:loop(S0)
    end.

validate({R,C,_M},0,Send,St = #s{gfx=Gfx}) ->
    if Send -> Gfx ! {correct, {R,C}}; true -> ok end,
    St;
validate(RCM={R,C,_M},Val,Send,St = #s{gfx=Gfx,v=Vs}) ->
    S = poss(RCM,St),
    case gb_sets:is_member(Val,S) of
	true ->
	    if Send -> Gfx ! {correct, {R,C}}; true -> ok end,
	    add(RCM,Val,St);
	false ->
	    if Send -> Gfx ! {wrong, {R,C}}; true -> ok end,
	    St#s{v=setelement(indx(R,C),Vs,Val)}
    end.

rebuild_all(_, S0) ->
    Solved = get_known(S0),
    S1 = empty_table(S0),
    lists:foldl(fun({Indx,Val},Acc) ->
			add(rcm(Indx),Val,Acc)
		end, S1, Solved).

test() ->  %% Known to solvable
    [{{1,2},6}, {{1,4},1}, {{1,6},4}, {{1,8},5}, 
     {{2,3},8}, {{2,4},3}, {{2,6},5}, {{2,7},6}, 
     {{3,1},2}, {{3,9},1}, 
     {{4,1},8}, {{4,4},4}, {{4,6},7}, {{4,9},6},
     {{5,3},6}, {{5,7},3},
     {{6,1},7}, {{6,4},9}, {{6,6},1}, {{6,9},4}, 
     {{7,1},5}, {{7,9},2}, 
     {{8,3},7}, {{8,4},2}, {{8,6},6}, {{8,7},9}, 
     {{9,2},4}, {{9,4},5}, {{9,6},8}, {{9,8},7}].

new_game(S) ->
    rand:seed(exsplus),
    case new_game(1,1,gb_sets:empty(),empty_table(S#s{}),[], 0) of
	stop -> new_game(S); 
	Game -> Game
    end.
    

new_game(_,_,_,_St,_Acc,Cnt) when Cnt > 200 -> 
    %% Backtracked 200 times, Bad path lets start over
    stop;
new_game(R,C,BT,St,Acc,Cnt) when R < 10, C < 10 ->
    M = mat(R,C),
    U = poss({R,C,M},St),
    S = gb_sets:difference(U,BT),
    case gb_sets:size(S) of
	0 ->
	    [{{BR,BC},BVal,BBT,BST}|BAcc] = Acc,
	    new_game(BR,BC,gb_sets:add(BVal,BBT),BST,BAcc,Cnt+1);
	Size -> 
	    Ind = rand:uniform(Size),
	    V = lists:nth(Ind,gb_sets:to_list(S)),
	    new_game(R,C+1,gb_sets:empty(),
		     add({R,C,M},V,St),		    
		     [{{R,C},V,BT,St}|Acc], Cnt)
    end;
new_game(R,_C,Bt,S,Acc,Cnt) when R < 10 ->
    new_game(R+1,1,Bt,S,Acc,Cnt);
new_game(_,_,_,S,_Acc,_Cnt) -> 
%%    io:format("Backtracked ~p ~n",[_Cnt]),
    S.

pick_shown(S0, Level, Gfx) ->
    Given = gb_sets:from_ordset([I || I <- lists:seq(1,9*9)]),
    get_known(pick_shown(Given,Given,S0,level(Level),Gfx)).

get_known(#s{v=Vals}) -> 
    lists:foldl(fun(Index,Acc) ->
			case element(Index,Vals) of
			    0 -> Acc;
			    Val -> 
				{R,C,_} = rcm(Index),
				[{{R,C},Val}|Acc]
			end
		end, [], lists:seq(1,9*9)).

pick_shown(Given,Left,S0,Level,Gfx) ->    
    LeftSz  = gb_sets:size(Left),
    GivenSz = gb_sets:size(Given),
    if LeftSz == 0 -> 
	    io:format("No left ~p~n", [GivenSz]),
            S0;
       GivenSz < Level -> 
	    io:format("Below level ~p ~p~n", [GivenSz,Level]),
            S0;
       true ->
	    Ran = rand:uniform(LeftSz),
	    V   = lists:nth(Ran,gb_sets:to_list(Left)),
	    S1  = rebuild_all(rcm(V),S0#s{v=setelement(V,S0#s.v,0)}),
	    case solve(S1, true) of
		{true, _, _} -> 
		    catch Gfx ! {working, 100-LeftSz},
		    pick_shown(gb_sets:delete(V,Given), 
			       gb_sets:delete(V,Left), 
			       S1, Level,Gfx);
		{false,_,_} ->
		    pick_shown(Given,gb_sets:delete(V,Left), 
			       S0, Level,Gfx)
	    end
    end.

solve(St=#s{v=Vals},All) ->
    Unsolved = [I || I <- lists:seq(1,9*9), element(I,Vals) == 0],
    solve(Unsolved, All, St, [], [], lists:reverse(Unsolved)).

solve(Rem, false, _St, [Solved|_], Unsolved, _) -> {true, [Solved], Rem ++ Unsolved};
solve([], _, _St, Solved, [], _) ->                {true, Solved, []};
solve([], _, _St, Solved, Unsolved, Unsolved) ->   {false, Solved, Unsolved};
solve([], _, St, Solved, Unsolved, _Orig) -> 
    solve(Unsolved,true,St,Solved,[],lists:reverse(Unsolved));
solve([Index|Rest],All, St, S, US, Orig) ->
    RCM = rcm(Index),
    Poss = poss(RCM,St),
    case gb_sets:size(Poss) of 
	1 ->
	    %%	    io:format("S1 ~n",[]),
	    [Val] = gb_sets:to_list(Poss),
	    solve(Rest, All, add(RCM,Val,St), [{Index,Val}|S],US,Orig);
	_ ->
	    case solve_1(RCM, Poss, St) of
		false ->
		    solve(Rest, All, St, S, [Index|US],Orig);
		Val ->
		    solve(Rest, All, add(RCM,Val,St), [{Index,Val}|S],US,Orig)
	    end
    end.

solve_1(RCM={R,C,_M}, Avail, St) ->
    All = all(RCM),
    Poss = fun({RI,CI},Acc) when (RI == R) and (CI == C) -> Acc;
	      ({RI,CI},Acc) -> gb_sets:union(poss(rcm({RI,CI}),St),Acc)
	   end, 
    D = fun({RI,CI},Acc) when (RI == R) and (CI == C) -> 
		io:format("~p:~p: ignore~n",[RI,CI]), 
		Acc;	      
	   ({RI,CI},Acc) -> 
		Res = gb_sets:union(poss(rcm({RI,CI}),St),Acc),
		io:format("~p:~p: ~p => ~p ~n",[RI,CI,gb_sets:to_list(poss(rcm({RI,CI}),St)),gb_sets:to_list(Res)]), 
		Res
	end, 
    solve_2(All,{Poss,D},Avail).  

solve_2([],_, _) -> false;
solve_2([First|R],{Poss,D},Avail) -> 
    All = lists:foldl(Poss, gb_sets:empty(), First),
    Res = gb_sets:difference(Avail, All),
    case gb_sets:size(Res) of
	1 ->
	    %%	    lists:foldl(D, gb_sets:empty(), First),
	    %%	    io:format("Poss: ~w~nA: ~p O:~p ~n",[First,gb_sets:to_list(Avail),gb_sets:to_list(All)]),
	    [Val] = gb_sets:to_list(Res),
	    Val;
	_ ->
	    solve_2(R,{Poss,D},Avail)
    end.

all({RI,CI,MI}) -> all(RI,CI,MI).
all(RI,CI,MI) ->
    MR = ((MI-1) div 3)*3,
    MC = ((MI-1) rem 3)*3,
    Ri = [{RI,N} || N <- lists:seq(1,9)],
    Ci = [{N,CI} || N <- lists:seq(1,9)],
    Mi = [{1+MR,1+MC},{1+MR,2+MC},{1+MR,3+MC},
	  {2+MR,1+MC},{2+MR,2+MC},{2+MR,3+MC},
	  {3+MR,1+MC},{3+MR,2+MC},{3+MR,3+MC}],
    [Ri,Ci,Mi].

other_mats(N) -> 
    if N < 4 -> P1=3, P2= 6;
       N < 7 -> P1=-3,P2= 3;
       true  -> P1=-6,P2=-3
    end,
    case (N-1) rem 3 of
	0 -> [N+1,N+2,N+P1,N+P2];
	1 -> [N-1,N+1,N+P1,N+P2];
	2 -> [N-2,N-1,N+P1,N+P2]
    end.

check_must(S=#s{p=Poss,m=MS,mr=MR0,mc=MC0}) ->
    List = lists:seq(1,9),
    {MR,MC} = lists:foldl(fun(Val,{MRT,MCT}) ->
				  check_must2(List,Val,Poss,MS,MRT,MCT)
			  end, {MR0,MC0}, List),
    S#s{mr=MR,mc=MC}.

check_must2([M|Rest],Val,Poss,Ms,MR0,MC0) ->
    case gb_sets:is_member(Val, element(M,Ms)) of
	true ->
	    {Rows,Cols} = rc_in_mat(M),
	    MR1  = check_must3(Rows,Val,Poss,row,MR0),
	    MC1  = check_must3(Cols,Val,Poss,col,MC0),
	    check_must2(Rest,Val,Poss,Ms,MR1,MC1);
	false ->
	    check_must2(Rest,Val,Poss,Ms,MR0,MC0)
    end;
check_must2([],_,_,_,MR,MC) -> {MR,MC}.

check_must3({F1,F2,F3},Val,Check,Type,Must0) ->
    R1 = not gb_sets:is_member(Val, get_poss(F1,Check,gb_sets:empty())),
    R2 = not gb_sets:is_member(Val, get_poss(F2,Check,gb_sets:empty())),
    R3 = not gb_sets:is_member(Val, get_poss(F3,Check,gb_sets:empty())),
    %%    io:format("M=~p ~p ~p ~p ~p~n",[M,[R1,R2,R3],gb_sets:to_list(element(F1,Check)),gb_sets:to_list(element(F2,Check)),gb_sets:to_list(element(F3,Check))]),  
    if R1,R2 -> update_must(Type,F3,Val,Must0);
       R1,R3 -> update_must(Type,F2,Val,Must0);
       R2,R3 -> update_must(Type,F1,Val,Must0);
       true  -> Must0
    end.

update_must(Type,[Indx|_],Val,Must) ->
    N = mindx(Type, Indx),
    %%    io:format("~p ~p ~p must contain ~p~n",[Type,N,rcm(Indx),Val]),
    Set = element(N,Must),
    setelement(N,Must, gb_sets:add(Val,Set)).

add(RCM={R,C,M},Val,S=#s{p=P0,m=MS,v=Vals,mr=MR0,mc=MC0}) ->
    Ri = mindx(R,M),
    Ci = mindx(M,C),
    MR = delete(Val,Ri,MR0),
    MC = delete(Val,Ci,MC0),
    P1 = setelement(indx(RCM),P0,gb_sets:empty()),
    check_must(S#s{p=delete(Val,lists:flatten(all(RCM)),P1),
		   m=delete(Val,M,MS),
		   mr=MR,mc=MC,
		   v=setelement(indx(RCM),Vals,Val)}).

poss(RCM={R,C,M}, #s{p=P,v=Vals,mr=MR,mc=MC}) ->
    I = indx(R,C),
    case element(I, Vals) of
	0 ->
	    Rm = mindx(R,M),
	    Cm = mindx(M,C),
	    T1 = gb_sets:intersection(element(Rm,MR),element(Cm,MC)),
	    case gb_sets:size(T1) of
		1 -> T1;
		_ ->
		    Not = get_nots(RCM,MR,MC),
		    gb_sets:difference(element(I,P),Not)
	    end;
	_ ->
	    gb_sets:empty()
    end.

get_nots({R,C,M},MR,MC) ->		    
    [RM1,RM2,CM1,CM2] = other_mats(M),
    R1 = get_poss([mindx(R,RM1),mindx(R,RM2)],MR,gb_sets:empty()),
    R2 = get_poss([mindx(CM1,C),mindx(CM2,C)],MC,R1),
    %%     io:format("~p:~p:~p ~p ~p~n", 
    %% 	      [C,CM1,CM2,
    %% 	       gb_sets:to_list(element(mindx(CM1,C),MC)),
    %% 	       gb_sets:to_list(element(mindx(CM2,C),MC))]),
    R2.

get_poss([],_,Tot) -> Tot;
get_poss([H|R],What,Tot) ->
    %%     io:format("~p~n",[H]),
    get_poss(R,What, gb_sets:union(element(H,What),Tot)).

mindx(row,Indx) -> 
    {R,_C,M} = rcm(Indx),
    mindx(R,M);
mindx(col,Indx) -> 
    {_R,C,M} = rcm(Indx),
    mindx(M,C);

mindx(R,M) ->
    1+(R-1)*3 + (M-1) rem 3.

rcm(Indx) when is_integer(Indx) ->
    rcm({((Indx-1) div 9)+1, (Indx-1) rem 9+1});
rcm({R,C}) ->
    M = mat(R,C),
    {R,C,M}.
mat(R,C) ->
    1+(C-1) div 3 + ((R-1) div 3)*3.

rc_in_mat(M) -> 
    R1 = 1+3*((M-1) div 3),
    C1 = 1+3*((M-1) rem 3),
    {{[indx({R1+0,C1+0}),indx({R1+0,C1+1}),indx({R1+0,C1+2})],
      [indx({R1+1,C1+0}),indx({R1+1,C1+1}),indx({R1+1,C1+2})],
      [indx({R1+2,C1+0}),indx({R1+2,C1+1}),indx({R1+2,C1+2})]},

     {[indx({R1+0,C1+0}),indx({R1+1,C1+0}),indx({R1+2,C1+0})],
      [indx({R1+0,C1+1}),indx({R1+1,C1+1}),indx({R1+2,C1+1})],
      [indx({R1+0,C1+2}),indx({R1+1,C1+2}),indx({R1+2,C1+2})]}}.

indx(Indx) when is_integer(Indx) -> Indx;
indx({Row, Col}) ->
    indx(Row,Col);
indx({Row, Col,_}) ->
    indx(Row,Col).
indx(Row, Col) ->
    (Row-1)*9+Col.

delete(_Val,[],S0) -> S0;
delete(Val,[I1|R],S0) ->
    I = if is_integer(I1) -> I1;
	   true -> indx(I1)
	end,
    S = setelement(I,S0,gb_sets:delete_any(Val, element(I,S0))),
    delete(Val,R,S);
delete(Val,I,S) ->
    setelement(I,S,gb_sets:delete_any(Val, element(I,S))).

%%%%%%%%%%%%%%%%%%%%%%%%%

%% Pre generate games on low priority
create_games(Levels,Engine) ->
    gen_loop(Levels, Engine, 5).

gen_loop([], Engine,_) ->
    receive 
	{gen_game, Level} ->
	    gen_loop([Level], Engine,5)
    end;
gen_loop([Level|Ls], Engine, N) when N > 0 ->    
    Empty = empty_table(#s{}),
    Temp = new_game(Empty),
    Game = pick_shown(Temp,Level,undefined),    
    ResLev = length(Game),
    Engine ! {game, {ResLev, Game}},
    case ResLev =< level(Level) of
	true -> 
	    gen_loop(Ls,Engine, 5);
	false -> 
	    gen_loop([Level|Ls],Engine, N-1)
    end;
gen_loop([_|Ls],Engine, _) ->
    gen_loop(Ls,Engine, 5).

find_game(_, #s{games=[]}) -> false;
find_game(hardest, S = #s{games=Gs0}) ->
    Hard = level(hard),
    case lists:sort(Gs0) of
	[{Level,G}|Gs] when Level < (Hard-5) -> 
	    {G, S#s{games=Gs}};
	_ -> false
    end;
find_game(Level, S = #s{games=Gs0}) -> 
    case find_game2(level(Level), lists:reverse(lists:sort(Gs0)), []) of
	false -> false;
	{Game, Gs} -> {Game,S#s{games=Gs}}
    end.

find_game2(Hard, [{Level,G}|Gs], Acc) when Level =< Hard, Level > (Hard-5)  -> 
    {G, Gs ++ Acc};
find_game2(Hard, [G|Gs], Acc) -> 
    find_game2(Hard, Gs, [G|Acc]);
find_game2(_Hard, [], _ ) -> false.
		   	    
levels() ->
    [trivial,easy,normal,hard,hardest].

level(Level) when is_atom(Level) ->
    case Level of
	all ->   100;
	trivial -> 40;
	easy ->   35;
	normal -> 30;
	hard ->   25;
	hardest -> 0
    end;
level(Int) when is_integer(Int) ->
    if 
	Int =< 20 -> hardest;
	Int =< 25 -> hard;
	Int =< 30 -> normal;
	Int =< 35 -> easy;
	true -> trivial
    end.
	     

     
