%%% -*- coding: utf-8; erlang-indent-level: 2 -*-
%%% -------------------------------------------------------------------
%%% Copyright 2010-2021 Manolis Papadakis <manopapad@gmail.com>,
%%%                     Eirini Arvaniti <eirinibob@gmail.com>,
%%%                 and Kostis Sagonas <kostis@cs.ntua.gr>
%%%
%%% This file is part of PropEr.
%%%
%%% PropEr 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.
%%%
%%% PropEr 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 PropEr.  If not, see <http://www.gnu.org/licenses/>.

%%% @copyright 2010-2021 Manolis Papadakis, Eirini Arvaniti, and Kostis Sagonas
%%% @version {@version}
%%% @author Manolis Papadakis
%%% @doc PropEr usage example: Static mastermind solver

-module(mastermind).
-export([solve/3, solve/4]).

%% O-ary props are automatically exported; 1-ary ones are not
-export([prop_all_produced_solutions_are_valid/1,
	 prop_secret_combination_is_not_discarded/1,
	 prop_invalidated_instances_reject_original_secret/1]).

-include_lib("proper/include/proper.hrl").
-include_lib("eunit/include/eunit.hrl").

%% -----------------------------------------------------------------------------
%% Types
%% -----------------------------------------------------------------------------

-define(MAX_LENGTH, 13).

-type len()        :: 2..?MAX_LENGTH.
-type num_colors() :: 1..36.
-type blacks()     :: 0..?MAX_LENGTH.
-type whites()     :: 0..?MAX_LENGTH.
-type comb()       :: string().  % XXX: can be further refined
-type rawguess()   :: {comb(),blacks(),whites()}.
-type rawguesses() :: [rawguess()].
-type solver()     :: 'heur' | 'simple' | 'stream'.


%% -----------------------------------------------------------------------------
%% Problem statement
%% -----------------------------------------------------------------------------

%% Given a list of guesses for the secret combination in a game of Mastermind
%% and their corresponding score of black and white pegs, find the first
%% combination that is compatible with all the guess-score pairs (the order of
%% combinations is derived lexicographically from the order of colors).

%% Colors are represented as hex digits, but we allow the use of letters up to
%% 'z' - thus, there may be up to 36 colors ('0' - '9' and 'a' - 'z'). The
%% combinations are represented as strings of such characters. That is also the
%% expected format for the answer. If there is no combination compatible with
%% all the guesses, the program should return the string "-1".

%% The module should export a `solve` function that takes the following
%% arguments:
%% 1) the length of the combinations (> 1)
%% 2) the number of colors (1..36)
%% 3) the list of guess-score triples, in the format:
%%    {guess, num_black_pegs, num_white_pegs}

%% Expected output:
%% mastermind:solve(4, 10, [{"3157",1,2},{"1350",2,1},{"6120",0,2},{"2381",3,0}]).
%% "2351"
%% mastermind:solve(4, 10, [{"3557",1,2},{"1350",2,1},{"6120",0,2},{"2381",3,0}]).
%% "-1"
%% mastermind:solve(4, 10, [{"3557",1,2},{"1350",0,1},{"2575",2,1},{"5574",3,0}]).
%% "5576"
%% mastermind:solve(5, 10, [{"12345",1,0},{"02789",1,2},{"82900",3,0}]).
%% "22902"
%% mastermind:solve(5, 10, [{"23543",0,2},{"45674",1,2},{"67242",2,0}]).
%% "67375"
%% mastermind:solve(5, 10, [{"74562",0,0},{"11300",1,0}]).
%% "18888"
%% mastermind:solve(4, 10, [{"1234",1,0},{"0004",1,0},{"0222",0,0},{"4444",1,0},
%%			    {"5554",1,0},{"6664",2,0},{"6784",2,2}]).
%% "6874"
%% mastermind:solve(6, 10, [{"353523",0,5},{"294333",3,2},{"254672",2,1}]).
%% "534332"
%% mastermind:solve(6, 10, [{"097654",1,3},{"000465",1,1},{"011579",0,2},
%%			    {"227496",1,3},{"347963",4,1}]).
%% "467963"
%% mastermind:solve(6, 10, [{"006892",0,2},{"115258",2,2},{"357368",2,1}]).
%% "112365"
%% mastermind:solve(7, 10, [{"2104767",1,3},{"3541285",3,1},{"7567128",1,4},
%%			    {"0117285",1,4},{"1521775",2,2},{"3261781",4,0}]).
%% "3570781"
%% mastermind:solve(8, 10, [{"11244556",0,2},{"66756572",1,4},{"00026667",1,3},
%%			    {"03663775",1,3},{"22677262",0,3},{"67568688",7,0}]).
%% "67568689"
%% mastermind:solve(8, 10, [{"21244767",3,0},{"35455685",3,1},{"75687658",2,4}]).
%% "05258667"
%% mastermind:solve(8, 10, [{"76897034",5,0},{"76284933",3,2}]).
%% "06097033"
%% mastermind:solve(9, 10, [{"345352352",0,5},{"287639433",3,2},{"276235467",5,2},
%%			    {"523459878",0,5}]).
%% "082235466"
%% mastermind:solve(10, 10, [{"3476453523",0,5},{"2876394333",3,2},
%%			     {"2762354672",5,2},{"5234598781",0,5}]).
%% "0122374372"


%% -----------------------------------------------------------------------------
%% Utility functions
%% -----------------------------------------------------------------------------

%% Function: all_combinations/2
%% Produces all 'Len'-length combinations made up of colors selected from
%% 'ColorsList'.
all_combinations(Len, ColorsList) ->
    all_combinations_tr(Len, ColorsList, [[]]).

all_combinations_tr(0, _ColorsList, Acc) ->
    Acc;
all_combinations_tr(Left, ColorsList, Acc) ->
    NewAcc = [[Color|Rest]  || Color <- ColorsList, Rest <- Acc],
    all_combinations_tr(Left - 1, ColorsList, NewAcc).

%% Function: all_selections/2
%% Returns all possible selections of 'N' elements from list 'List'.
all_selections(0, _List) ->
    [[]];
all_selections(N, List) when N >= 1 ->
    Len = length(List),
    case N > Len of
	true ->
	    erlang:error(badarg);
	false ->
	    all_selections(N, List, Len)
    end.

all_selections(1, List, _Len) ->
    [[X] || X <- List];
all_selections(Take, List, Len) when Take =:= Len ->
    [List];
all_selections(Take, [Head|Tail], Len) ->
    [[Head|Rest] || Rest <- all_selections(Take - 1, Tail, Len - 1)]
    ++ all_selections(Take, Tail, Len - 1).

%% Function: all_selection_pos/2
%% Returns all possible selections of 'N' positions from a 'Len'-length list.
all_selection_pos(N, Len) ->
    all_selections(N, lists:seq(1,Len)).

%% Function: remove/2
%% Removes from a list, 'List', the elements at positions 'Positions'. Returns
%% both the resulting list and a list of the removed elements, in the same
%% order they were removed.
%% Note that the positions must be given in order.
remove(Positions, List) ->
    remove_tr(Positions, List, 1, [], []).

remove_tr([], List, _CurrPos, Kept, Removed) ->
    {lists:reverse(Kept) ++ List, lists:reverse(Removed)};
remove_tr([CurrPos|PosTail], [X|ListTail], CurrPos, Kept, Removed) ->
    remove_tr(PosTail, ListTail, CurrPos + 1, Kept, [X|Removed]);
remove_tr(Positions, [X|ListTail], CurrPos, Kept, Removed) ->
    remove_tr(Positions, ListTail, CurrPos + 1, [X|Kept], Removed).

%% Function: insert/3
%% Inserts into a list, 'List', the elements of 'ToInsert', in the corresponding
%% positions, 'Positions'.
%% Note that the positions must be given in order.
insert(Positions, ToInsert, List) ->
    insert_tr(Positions, ToInsert, List, 1, []).

insert_tr([], [], List, _CurrPos, Acc) ->
    lists:reverse(Acc) ++ List;
insert_tr([CurrPos|PosTail], [X|ToInsertTail], List, CurrPos, Acc) ->
    insert_tr(PosTail, ToInsertTail, List, CurrPos + 1, [X|Acc]);
insert_tr(Positions, ToInsert, [X|ListTail], CurrPos, Acc) ->
    insert_tr(Positions, ToInsert, ListTail, CurrPos + 1, [X|Acc]).

%% Function: delete/2
%% Removes from a list, 'List', a subsequence of that list, 'ToDelete'.
delete(List, ToDelete) ->
    delete_tr(List, ToDelete, []).

delete_tr(List, [], Acc) ->
    lists:reverse(Acc) ++ List;
delete_tr([X|ListTail], [X|ToDeleteTail], Acc) ->
    delete_tr(ListTail, ToDeleteTail, Acc);
delete_tr([X|Rest], ToDelete, Acc) ->
    delete_tr(Rest, ToDelete, [X|Acc]).

%% Function: insert_all/2
%% Returns all possible insertions of the elements of the first list inside the
%% second list.
insert_all([], List) ->
    [List];
insert_all([X|Rest], List) ->
    [L2 || L1 <- insert_all(Rest, List), L2 <- all_insertions(X, L1)].

%% Function: all_insertions/2
%% Returns all possible insertions of 'X' inside 'List'.
all_insertions(X, List) ->
    all_insertions_tr(X, [], List, []).

all_insertions_tr(X, Front, [], Acc) ->
    [Front ++ [X] | Acc];
all_insertions_tr(X, Front, Back = [BackHead|BackTail], Acc) ->
    all_insertions_tr(X, Front ++ [BackHead], BackTail,
		      [Front ++ [X] ++ Back | Acc]).

%% Function true_permutation/2
%% Returns true iff two permutations of the same list have no element in the
%% same position.
true_permutation([], []) ->
    true;
true_permutation([H|_NewTail], [H|_OldTail]) ->
    false;
true_permutation([_NewHead|NewTail], [_OldHead|OldTail]) ->
    true_permutation(NewTail, OldTail).


%% -----------------------------------------------------------------------------
%% Solver code
%% -----------------------------------------------------------------------------

%% Function: compatible/4
%% Tests whether combination A produces the given score when compared against
%% combination B. This is always the same as when combination B is compared
%% against combination A.
compatible(A, B, {Blacks,Whites}, Colors) ->
    correct_blacks(A, B, Blacks)
    andalso correct_sum(A, B, Blacks + Whites, Colors).

correct_blacks([], [], 0) -> true;
correct_blacks([], [], _N) -> false;
correct_blacks([Ah|_At], [Bh|_Bt], 0) when Ah =:= Bh -> false;
correct_blacks([Ah|At], [Bh|Bt], N) when Ah =:= Bh ->
    correct_blacks(At, Bt, N - 1);
correct_blacks([_Ah|At], [_Bh|Bt], N) -> correct_blacks(At, Bt, N).

correct_sum(A, B, N, Colors) ->
    AFreqs = collect_freqs(A, Colors),
    BFreqs = collect_freqs(B, Colors),
    Common = lists:zipwith(fun erlang:min/2, AFreqs, BFreqs),
    lists:sum(Common) =:= N.

collect_freqs(Combination, Colors) ->
    lists:foldl(fun(C,F) -> inc_freq(C,F) end, lists:duplicate(Colors,0),
		Combination).

inc_freq(Color, Freqs) ->
    {H,[OldFreq | T]} = lists:split(Color, Freqs),
    H ++ [OldFreq + 1] ++ T.

%% Function: score/2
%% Compares two combinations A and B and calculates the corresponding score.
%% A and B must be of the same length and color number. The order of the
%% arguments is not important (i.e. it is always score(A,B) = score(B,A)).
%% This implementation is sub-optimal on purpose.
score(A, B) ->
    {Blacks,AA,BB} = remove_sames(A, B),
    Whites = get_whites(AA, BB),
    {Blacks, Whites}.

remove_sames(A, B) ->
    remove_sames_tr(A, B, 0, [], []).

remove_sames_tr([], [], N, AccA, AccB) ->
    {N, AccA, AccB};
remove_sames_tr([Ah|At], [Bh|Bt], N, AccA, AccB) when Ah =:= Bh ->
    remove_sames_tr(At, Bt, N + 1, AccA, AccB);
remove_sames_tr([Ah|At], [Bh|Bt], N, AccA, AccB) ->
    remove_sames_tr(At, Bt, N, [Ah|AccA], [Bh|AccB]).

get_whites(A, B) ->
    SA = lists:sort(A),
    SB = lists:sort(B),
    get_whites_tr(SA, SB, 0).

get_whites_tr([], _B, N) ->
    N;
get_whites_tr(_A, [], N) ->
    N;
get_whites_tr([Ah|At], [Bh|Bt], N) when Ah =:= Bh ->
    get_whites_tr(At, Bt, N + 1);
get_whites_tr([Ah|At], B = [Bh|_Bt], N) when Ah < Bh ->
    get_whites_tr(At, B, N);
get_whites_tr(A = [Ah|_At], [Bh|Bt], N) when Ah > Bh ->
    get_whites_tr(A, Bt, N).

%% Function: solve/3
%% Main entry function, serves as input/output filter for an actual solver
%% function, which must return a list of combinations that are compatible with
%% every guess-score pair provided. Such a list needn't be sorted - actually,
%% it needn't even be complete (i.e. containing all plausible secret
%% combinations), but it must contain the minimum combination compatible with
%% the input, if such a combination exists (being complete, however, helps with
%% testing).
-spec solve(len(), num_colors(), rawguesses()) -> comb().
solve(Len, Colors, RawGuesses) ->
    solve(Len, Colors, RawGuesses, heur).

%% Function: solve/4
%% The last argument is used to select a particular solver - valid solvers are
%% 'simple', 'stream' and 'heur', default is 'heur'.
-spec solve(len(), num_colors(), rawguesses(), solver()) -> comb().
solve(Len, Colors, RawGuesses, SolverName) ->
    Guesses = [{parse(RawComb),{B,W}} || {RawComb,B,W} <- RawGuesses],
    case valid_input(Len, Colors, Guesses) of
	true  -> ok;
	false -> erlang:error(badarg)
    end,
    Solver = get_solver(SolverName),
    Result = case Solver(Len, Colors, Guesses) of
		 [] -> error;
		 L  -> lists:min(L)
	     end,
    export(Result).

parse(RawComb) ->
    [digit_to_integer(X) || X <- RawComb].

export(error) ->
    "-1";
export(Comb) ->
    [integer_to_digit(X) || X <- Comb].

digit_to_integer(X) when X >= $0, X =< $9 -> X - $0;
digit_to_integer(X) when X >= $a, X =< $z -> X - $a + 10;
digit_to_integer(X) when X >= $A, X =< $Z -> X - $A + 10.

integer_to_digit(X) when X >= 0, X =< 9   -> X + $0;
integer_to_digit(X) when X >= 10, X =< 35 -> X - 10 + $a.

valid_input(Len, Colors, Guesses) ->
    Len > 0 andalso Colors > 0
    andalso lists:all(fun(G) -> valid_guess(Len, Colors, G) end, Guesses).

valid_guess(Len, Colors, {Comb,{Blacks,Whites}}) ->
    Blacks >= 0 andalso Whites >= 0
    andalso (Blacks + Whites < Len
	     orelse Blacks + Whites =:= Len andalso Whites =/= 1)
    andalso length(Comb) =:= Len
    andalso lists:all(fun(X) -> X >= 0 andalso X =< Colors end, Comb).

get_solver(SolverName) ->
    case SolverName of
	simple -> fun simple_solver/3;
	stream -> fun stream_solver/3;
	heur   -> fun heur_solver/3
    end.

%% Function: simple_solver/3
%% Simple way to produce all combinations which are compatible with a given
%% list of guess-score pairs:
%% * create a list of all possible 'Len'-length combinations of 'Colors' colors
%% * filter the list with all provided guess-score pairs (for each pair, we
%%   remove from the list those combinations that are incompatible with it)
%% Note that the resulting list is always complete and sorted.
simple_solver(Len, Colors, Guesses) ->
    Combs = all_combinations(Len, lists:seq(0,Colors-1)),
    filter_guesses(Colors, Guesses, Combs).

filter_guesses(_Colors, _Guesses, []) ->
    [];
filter_guesses(_Colors, [], Combs) ->
    Combs;
filter_guesses(Colors, [{Guess,Score} | Rest], Combs) ->
    IsCompatible = fun(C) -> compatible(Guess, C, Score, Colors) end,
    NewCombs = lists:filter(IsCompatible, Combs),
    filter_guesses(Colors, Rest, NewCombs).

%% Function: stream_solver/3
%% Low-memory solver: lazily produces and checks all possible combinations in
%% order until it finds one that is compatible with all guess-score pairs.
%% Note that the resulting list is almost certainly incomplete, since we only
%% return the first instance we find.
stream_solver(Len, Colors, Guesses) ->
    stream_solver_tr(Colors, Guesses, lists:duplicate(Len,0)).

stream_solver_tr(_Colors, _Guesses, done) ->
    [];
stream_solver_tr(Colors, Guesses, Comb) ->
    case lists:all(fun({C,S}) -> compatible(C,Comb,S,Colors) end, Guesses) of
	true  -> [Comb];
	false -> stream_solver_tr(Colors, Guesses, next_comb(Colors,Comb))
    end.

next_comb(Colors, Comb) ->
    next_comb_tr(Colors - 1, lists:reverse(Comb), []).

next_comb_tr(_MaxColor, [], _Acc) ->
    done;
next_comb_tr(MaxColor, [MaxColor | Rest], Acc) ->
    next_comb_tr(MaxColor, Rest, [0 | Acc]);
next_comb_tr(_MaxColor, [X | Rest], Acc) ->
    lists:reverse(Rest) ++ [X+1] ++ Acc.

%% Function: heur_solver/3
%% More sophisticated solver (avoids the construction of all possible
%% combinations):
%% * if the guess list is empty, return [[0,0,...,0]], else:
%% * sort the guesses by applying a selectivity heuristic (guesses whose
%%   score will result in more combinations being rejected are prefered)
%% * take the first guess-score pair and produce all the combinations it's
%%   compatible with
%% * filter the list with the rest of the pairs
%% Note that the resulting list is always complete (except for the special case
%% when Guesses =:= []) but is not necessarily sorted.
heur_solver(Len, _Colors, []) ->
    [lists:duplicate(Len, 0)];
heur_solver(Len, Colors, Guesses) ->
    [First|Rest] = lists:sort(fun(A,B) -> more_selective(A,B,Colors) end,
			      Guesses),
    Combs = all_compatibles(Len, Colors, First),
    filter_guesses(Colors, Rest, Combs).

%% Function: more_selective/2
%% Selectivity heuristic used to sort guess-score pairs. We suspect that
%% guess-score pair A is more selective than B if:
%% 1) it has a greater total score
%% 2) it has more black pegs
%% 3) it has fewer distinct colors
%% The above criteria are processed in that exact order.
more_selective({CombA,{BlacksA,WhitesA}}, {CombB,{BlacksB,WhitesB}}, Colors) ->
    case sign((BlacksA + WhitesA) - (BlacksB + WhitesB)) of
	+1 -> true;
	-1 -> false;
	0  -> case sign(BlacksA - BlacksB) of
		  +1 -> true;
		  -1 -> false;
		  0  -> distinct_colors(CombA, Colors)
			=< distinct_colors(CombB, Colors)
	      end
    end.

sign(0)            -> 0;
sign(X) when X > 0 -> +1;
sign(X) when X < 0 -> -1.

distinct_colors(Comb, Colors) ->
    lists:foldl(fun(F,S) -> sign(F) + S end, 0, collect_freqs(Comb, Colors)).

%% Function: all_compatibles/3
%% Runs the 'all_whites' function for all possible selections of 'Blacks'
%% positions in the given combination.
all_compatibles(Len, Colors, {Comb,{Blacks,Whites}}) ->
    NonFixedLen = Len - Blacks,
    [C || BlackSelPos <- all_selection_pos(Blacks, Len),
	  C <- all_whites(NonFixedLen, Whites, Colors, Comb, BlackSelPos)].

all_whites(NonFixedLen, Whites, Colors, Comb, BlackSelPos) ->
    RejectedLen = NonFixedLen - Whites,
    {NonFixed,Fixed} = remove(BlackSelPos, Comb),
    UnsortedWhiteSels =
	[{Sel,lists:sort(Sel)} || Sel <- all_selections(Whites, NonFixed)],
    WhiteSels = lists:ukeysort(2, UnsortedWhiteSels),
    [insert(BlackSelPos, Fixed, C)
	|| {WhiteSel,_} <- WhiteSels,
	   C <- all_moves(NonFixed, WhiteSel, RejectedLen, Colors)].

all_moves(NonFixed, WhiteSel, RejectedLen, Colors) ->
    Rejected = delete(NonFixed, WhiteSel),
    RemainingColors = lists:seq(0,Colors-1) -- Rejected,
    AllCombs = all_combinations(RejectedLen, RemainingColors),
    UnsortedAllMoves = [L || C <- AllCombs,
			     L <- insert_all(WhiteSel, C),
			     true_permutation(L, NonFixed)],
    lists:usort(UnsortedAllMoves).


%% -----------------------------------------------------------------------------
%% Properties to check
%% -----------------------------------------------------------------------------

prop_all_combinations_are_produced() ->
    ?FORALL({Len, ColorsList},
	    {range(0,5), short_nd_list(integer())},
	    begin
		AllCombs = all_combinations(Len, ColorsList),
		NumAllCombs = pow(length(ColorsList), Len),
		lofl_check(AllCombs, NumAllCombs, Len, ColorsList)
		andalso no_duplicates(AllCombs)
	    end).

short_nd_list(ElemType) ->
    ?LET(L,
	 resize(7, list(ElemType)),
	 lists:usort(L)).

lofl_check(Lofl, NumLists, ListLen, ListElems) ->
    lofl_check(Lofl, NumLists, ListLen, ListElems, 0).

lofl_check([], NumLists, _ListLen, _ListElems, Acc) ->
    Acc =:= NumLists;
lofl_check([List|Rest], NumLists, ListLen, ListElems, Acc) ->
    list_check(List, ListLen, ListElems)
    andalso lofl_check(Rest, NumLists, ListLen, ListElems, Acc + 1).

list_check([], 0, _Elems) ->
    true;
list_check([], _Left, _Elems) ->
    false;
list_check([X|Rest], Left, Elems) ->
    lists:member(X, Elems)
    andalso list_check(Rest, Left - 1, Elems).

pow(X, Y) ->
    pow_tr(X, Y, 1).

pow_tr(_X, 0, Acc) ->
    Acc;
pow_tr(X, Y, Acc) ->
    pow_tr(X, Y - 1, X * Acc).

no_duplicates(L) -> length(L) =:= length(lists:usort(L)).

prop_all_selections_are_produced() ->
    ?FORALL(List,
	    short_ne_list(integer()),
	    begin
		Len = length(List),
		?FORALL(N,
			range(0,Len),
			begin
			    AllSels = all_selections(N, List),
			    NumAllSels = num_sels(N, Len),
			    lofl_check(AllSels, NumAllSels, N, List)
			end)
	    end).

short_list(ElemType) ->
    resize(10, list(ElemType)).

short_ne_list(ElemType) ->
    non_empty(short_list(ElemType)).

num_sels(N, Len) ->
    fact(Len) div fact(N) div fact(Len - N).

fact(0) ->
    1;
fact(N) when N >= 1 ->
    N * fact(N-1).

prop_remove_insert_symmetry() ->
    ?FORALL(List,
	    short_ne_list(integer()),
	    ?FORALL(Positions,
		    pos_selection(List),
		    begin
			{Kept,Removed} = remove(Positions,List),
			insert(Positions,Removed,Kept) =:= List
		    end)).

pos_selection(List) ->
    Len = length(List),
    ?LET(N,
	 range(0,Len),
	 oneof(all_selection_pos(N, Len))).

prop_delete_insert_all_symmetry() ->
    ?FORALL(List,
	    short_list(integer()),
	    ?FORALL(Subseq,
		    subsequence(List),
		    lists:member(List,
				 insert_all(Subseq,delete(List,Subseq))))).

subsequence(List) ->
    ?LET(L,
	 [{X,boolean()} || X <- List],
	 [Y || {Y,true} <- L]).

prop_compatible_works() ->
    ?FORALL({Colors,A,B},
	    two_combinations(),
	    compatible(A, B, score(A,B), Colors)).

combination(Len, Colors) ->
    vector(Len, range(0,Colors-1)).

two_combinations() ->
    ?LET({Len, Colors},
	 {range(0,30), range(1,36)},
	 {Colors, combination(Len,Colors), combination(Len,Colors)}).

prop_io_filters_are_symmetric() ->
    ?FORALL(L,
	    list(digit()),
	    collect(num_digits(length(L)),
		    export(parse(L)) =:= L)).

digit() -> union([range($0,$9), range($a,$z)]).

num_digits(X) when X >= 0, X =< 9 -> 1;
num_digits(X) when X >= 10 -> 1 + num_digits(X div 10).

prop_next_comb_produces_all_combinations_in_order() ->
    ?FORALL({Len, Colors},
	    {range(0,5), range(1,10)},
	    list_is_produced(Colors, lists:duplicate(Len,0),
			     all_combinations(Len,lists:seq(0,Colors-1)))).

list_is_produced(_Colors, done, []) ->
    true;
list_is_produced(Colors, Same, [Same | Rest]) ->
    list_is_produced(Colors, next_comb(Colors,Same), Rest);
list_is_produced(_Colors, _Comb, _List) ->
    false.

prop_all_compatibles_are_produced() ->
    ?FORALL({Len, Colors, Guess},
	    one_guess_instance(),
	    simple_solver(Len, Colors, [Guess])
	    =:= lists:sort(all_compatibles(Len, Colors, Guess))).

one_guess_instance() ->
    ?LET({Len, Colors},
	 {range(2,5), range(2,10)},
	 {Len, Colors, scored_guess(Len,Colors)}).

scored_guess(Len, Colors) ->
    ?LET(Score,
	 valid_score(Len),
	 {combination(Len,Colors), Score}).

valid_score(Len) ->
    ?LET(Blacks,
	 range(0,Len),
	 ?LET(Whites,
	      ?SUCHTHAT(W,
			range(0,Len-Blacks),
			W =/= 1 orelse Blacks + W =/= Len),
	      {Blacks,Whites})).

prop_all_produced_solutions_are_valid(SolverName) ->
    Solver = get_solver(SolverName),
    ?FORALL({Len, Colors, Guesses},
	    instance(),
	    begin
		Solutions = Solver(Len, Colors, Guesses),
		collect(Solutions =:= [],
			lists:all(fun(Solution) ->
				      lists:all(fun({C,Score}) ->
						    compatible(C,Solution,
							       Score,Colors)
						end,
						Guesses)
				  end,
				  Solutions))
	    end).

instance() ->
    ?LET({Len, Colors},
	 {range(2,5), range(2,10)},
	 {Len, Colors, short_list(scored_guess(Len,Colors))}).

%% Note that the next property is not necessarily true for solvers that don't
%% return complete lists.
prop_secret_combination_is_not_discarded(SolverName) ->
    Solver = get_solver(SolverName),
    ?FORALL({Len,Colors,Secret,Guesses},
	    full_non_trivial_instance(),
	    lists:member(Secret, Solver(Len,Colors,Guesses))).

full_non_trivial_instance() ->
    ?LET({Len, Colors},
	 {range(2,5), range(2,10)},
	 ?LET({Secret, Guesses},
	      {combination(Len,Colors), short_ne_list(combination(Len,Colors))},
	      {Len,Colors,Secret,[{G,score(G,Secret)} || G <- Guesses]})).

prop_invalidated_instances_reject_original_secret(SolverName) ->
    Solver = get_solver(SolverName),
    ?FORALL({Len,Colors,Secret,Guesses},
	    invalid_instance(),
	    not lists:member(Secret, Solver(Len,Colors,Guesses))).

invalid_instance() ->
    ?LET({Len,Colors,Secret,Guesses},
	 full_non_trivial_instance(),
	 ?LET(Pos,
	      range(1,length(Guesses)),
	      begin
		  {Comb,OldScore} = lists:nth(Pos,Guesses),
		  ?LET(NewScore,
		       ?SUCHTHAT(S, valid_score(Len), S =/= OldScore),
		       {Len,Colors,Secret,
			list_update(Pos,{Comb,NewScore},Guesses)})
	      end)).

list_update(Index, NewElem, List) ->
    {H,[_OldElem | T]} = lists:split(Index - 1, List),
    H ++ [NewElem] ++ T.


%% -----------------------------------------------------------------------------
%% EUnit tests
%% -----------------------------------------------------------------------------

-define(_passes(Test),       ?_passes(Test, [])).
-define(_passes(Test, Opts), ?_assert(proper:quickcheck(Test, Opts))).

mastermind_props_0_test_() ->
  NamedProps0 =
    [{"All combinations", fun prop_all_combinations_are_produced/0},
     {"All selections", fun prop_all_selections_are_produced/0},
     {"Remove/Insert symmerty", fun prop_remove_insert_symmetry/0},
     {"Delete/Insert all symmetry", fun prop_delete_insert_all_symmetry/0},
     {"Compatible works", fun prop_compatible_works/0},
     {"IO filters symmetric", fun prop_io_filters_are_symmetric/0},
     {"Next comb", fun prop_next_comb_produces_all_combinations_in_order/0},
     {"All compatibles", fun prop_all_compatibles_are_produced/0}],
  [{N, ?_passes(P())} || {N,P} <- NamedProps0].

mastermind_props_1_test_() ->
  NamedProps1 =
    [{"Solutions valid", fun prop_all_produced_solutions_are_valid/1},
     %% fun prop_secret_combination_is_not_discarded/1,
     {"Invalidated instances",
      fun prop_invalidated_instances_reject_original_secret/1}],
  Strategies = [heur, simple, stream],
  [{"Secret comb using heur",
    ?_passes(prop_secret_combination_is_not_discarded(heur))},
   {"Secret comp using simple",
    ?_passes(prop_secret_combination_is_not_discarded(simple))}
   |
   [{N++" using "++atom_to_list(S), ?_passes(P(S))}
    || {N,P} <- NamedProps1, S <- Strategies]].
