%%% -*- coding: utf-8 -*-
%%% -*- erlang-indent-level: 2 -*-
%%% -------------------------------------------------------------------
%%% Copyright (c) 2017-2020, Andreas Löscher <andreas.loscher@it.uu.se>
%%%                     and  Kostis Sagonas <kostis@it.uu.se>
%%%
%%% 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 2017-2020 Andreas Löscher and Kostis Sagonas
%%% @version {@version}
%%% @author Andreas Löscher

-module(proper_gen_next).

-export([init/0, cleanup/0, from_proper_generator/1,
	 match/3, set_matcher/2, set_user_nf/2, update_caches/1]).

-export_type([matcher/0, temperature/0, depth/0, nf/0]).

-include("proper_internal.hrl").

-dialyzer({no_improper_lists, construct_improper/2}).

-define(GENERATORS, [{fun is_user_defined/1, fun user_defined_gen_sa/1}, %% needs to be first!
                     {fun is_atom/1, fun dont_change/1},
                     {fun is_list_type/1, fun list_gen_sa/1},
                     {fun is_fixed_list_type/1, fun fixed_list_gen_sa/1},
                     {fun is_integer_type/1, fun integer_gen_sa/1},
                     {fun is_float_type/1, fun float_gen_sa/1},
                     {fun is_atom_type/1, fun atom_gen_sa/1},
                     {fun is_vector_type/1, fun vector_gen_sa/1},
                     {fun is_tuple_type/1, fun tuple_gen_sa/1},
                     {fun is_binary_type/1, fun binary_gen_sa/1},
                     {fun is_binary_len_type/1, fun binary_len_gen_sa/1},
                     {fun is_let_type/1, fun let_gen_sa/1},
                     {fun is_shrink_list_type/1, fun shrink_list_gen_sa/1},
                     {fun is_union_type/1, fun union_gen_sa/1},
                     {fun is_wrapper_type/1, fun wrapper_gen_sa/1},
                     {fun is_exactly_type/1, fun exactly_gen_sa/1}
                    ]).

-define(TEMP(T), calculate_temperature(T)).
-define(SLTEMP(T), adjust_temperature(T)).

-define(GEN_NEXT_CACHE,  proper_gen_next_cache).
-define(GEN_NEXT_BACKUP, proper_gen_next_cache_backup).
-define(GEN_NEXT_DEPTH,  proper_gen_next_cache_depth).

-type temperature() :: float().
-type depth() :: pos_integer().
-type nf_temp() :: temperature() | {depth(), temperature()} | null.
-type nf() :: fun((term(), nf_temp()) -> proper_types:type()).
-type matcher() :: fun((term(), proper_types:raw_type(), temperature()) -> term()).

-spec update_caches('accept' | 'reject') -> 'ok'.
update_caches(accept) ->
  put(?GEN_NEXT_BACKUP, get(?GEN_NEXT_CACHE)),
  ok;
update_caches(reject) ->
  put(?GEN_NEXT_CACHE, get(?GEN_NEXT_BACKUP)),
  ok.

-spec from_proper_generator(proper_types:type()) -> proper_target:next_fun().
from_proper_generator(RawGenerator) ->
  ensure_initialized(),
  replace_generators(RawGenerator).

ensure_initialized() ->
  L = [?GEN_NEXT_CACHE, ?GEN_NEXT_BACKUP, ?GEN_NEXT_DEPTH, ?SEED_NAME,
       '$left', '$constraint_tries', '$typeserver_pid'],
  case lists:any(fun(X) -> get(X) =:= undefined end, L) of
    true ->
      %% not correctly initialized
      init(),
      proper:global_state_init_size(10),
      ok;
    _ ->
      ok
  end.

-spec init() -> ok.
init() ->
  init_pd(?GEN_NEXT_CACHE,  #{}),
  init_pd(?GEN_NEXT_BACKUP, #{}),
  init_pd(?GEN_NEXT_DEPTH,  #{max => 1}),
  ok.

-spec cleanup() -> ok.
cleanup() ->
  L = [?GEN_NEXT_CACHE, ?GEN_NEXT_BACKUP, ?GEN_NEXT_DEPTH],
  lists:foreach(fun(X) -> erase(X) end, L).

init_pd(Key, Value) ->
  case get(Key) of
    undefined ->
      put(Key, Value),
      ok;
    _ -> ok
  end.

-spec set_user_nf(proper_types:type(), nf()) -> proper_types:type().
set_user_nf(Type, NF) ->
  proper_types:add_prop(user_nf, NF, Type).

-spec set_matcher(proper_types:type(), matcher()) -> proper_types:type().
set_matcher(Type, Matcher) ->
  proper_types:add_prop(matcher, Matcher, Type).

store_max_depth(Depth) ->
  DS = get(?GEN_NEXT_DEPTH),
  #{max := CurrentMax} = DS,
  NewMax = max(Depth, CurrentMax),
  put(?GEN_NEXT_DEPTH, DS#{max => NewMax}),
  NewMax.

replace_generators(RawGen) ->
  Gen = proper_types:cook_outer(RawGen),
  case get_replacer(Gen) of
    {ok, Replacer} ->
      %% replaced generator
      UnrestrictedGenerator = Replacer(Gen),
      RestrictedGenerator = apply_constraints(UnrestrictedGenerator, Gen),
      TemperaturedGenerator = apply_temperature_scaling(RestrictedGenerator),
      apply_parameters(TemperaturedGenerator, Gen);
    _ ->
      %% fallback
      case proper_types:is_type(Gen) of
        true ->
          %% warning
          case get(proper_sa_testing) of
            true -> error(proper_sa_fallback);
            false ->
	      io:format("Fallback using regular generator instead: ~p~n", [Gen])
          end;
        false ->
          %% literal value -> no warning
          ok
      end,
      fun (_, _) -> Gen end
  end.

get_replacer(Type) ->
  get_replacer(Type, ?GENERATORS).

get_replacer(_, []) ->
  {error, type_not_found};
get_replacer(Type, [ {Guard, Replacer} | Tail]) ->
  case Guard(Type) of
    true -> {ok, Replacer};
    _ -> get_replacer(Type, Tail)
  end.

has_same_generator(LT, RT) ->
  case proper_types:find_prop(generator, LT) of
    {ok, LG} ->
      {ok, RG} = proper_types:find_prop(generator, RT),
      LG =:= RG;
    error -> false
  end.

apply_constraints(UnrestrictedGenerator, Type) ->
  fun (Base, Temp) ->
      Tries = get('$constraint_tries'),
      restrict_generation(UnrestrictedGenerator, Base, Temp, Tries, Type, none)
  end.

restrict_generation(_, _, T, 0, Type, none) ->
  %% it is possible to run out of tries but there are valid elements
  %% this is due to an interaction of matching and tuples/lists/...
  sample_from_type(Type, T);
restrict_generation(_, _, _, 0, _, {ok, WeakInstance}) -> WeakInstance;
restrict_generation(Gen, B, T, TriesLeft, Type, WeakInstance) ->
  Instance = Gen(B, T),
  CleanInstance = proper_gen:clean_instance(Instance),
  case proper_types:satisfies_all(CleanInstance, Type) of
    {true, true} ->
      %% strong
      Instance;
    {true, _} ->
      %% weak
      restrict_generation(Gen, B, T, TriesLeft - 1, Type, {ok, Instance});
    _ ->
      %% not at all
      restrict_generation(Gen, B, T, TriesLeft - 1, Type, WeakInstance)
  end.

apply_temperature_scaling(Generator) ->
  fun (Base, Temp) ->
      case Temp of
        {Depth, Temperature} -> Generator(Base, {Depth + 1, Temperature});
        null -> Generator(Base, null);
        _ -> Generator(Base, {1, Temp})
      end
  end.

apply_parameters(Generator, RawType) ->
  MaybeParameters = proper_types:find_prop(parameters, RawType),
  fun (Base, Temp) ->
      BaseType = ?LAZY(Generator(Base, Temp)),
      Type = case MaybeParameters of
               {ok, Parameters} ->
                 proper_types:with_parameters(Parameters, BaseType);
               error ->
                 BaseType
             end,
      {ok, Generated} = proper_gen:safe_generate(Type),
      Generated
  end.

adjust_temperature({Depth, Temperature}) ->
  NewDepth = Depth + 1,
  store_max_depth(NewDepth),
  {NewDepth, Temperature};
adjust_temperature(Temp) ->
  adjust_temperature({1, Temp}).

temperature_scaling(null, _) -> 0.0;
temperature_scaling(Temp, Depth) ->
  case get(proper_gen_next_temperature_scaling) of
    undefined -> 1.0;
    false -> 1.0;
    true ->
      M = 0.25,
      #{max := MaxD} = get(?GEN_NEXT_DEPTH),
      case MaxD of
        1 -> Temp;
        _ -> M + M*1/(1-MaxD) * Temp * (Depth-1)
      end
  end.

calculate_temperature(null) ->
  0;
calculate_temperature({Depth, Temp}) ->
  temperature_scaling(Temp, Depth);
calculate_temperature(Temp) ->
  temperature_scaling(Temp, 1).

%% sample
sample_from_type(Type, Temp) ->
  Gen = replace_generators(Type),
  {ok, Generated} = proper_gen:safe_generate(Type),
  Gen(Generated, Temp).

%% exactly
is_exactly_type(Type) ->
  proper_types:get_prop(kind, Type) =:= basic andalso
    not proper_types:is_type(proper_types:get_prop(env, Type)).

exactly_gen_sa({'$type', TypeProps}) ->
  {env, Value} = proplists:lookup(env, TypeProps),
  fun (_, _) -> Value end.

%% Numbers

%% utility functions
make_inrange(Val, L, R) when (R=:=inf orelse Val =< R) andalso (L=:=inf orelse Val >= L) -> Val;
make_inrange(Val, L, _R) when Val < L -> L;
make_inrange(Val, _L, R) when Val > R -> R.

make_inrange(Val, Offset, L, R) when L =/= inf, Val + Offset < L ->
  make_inrange(Val - Offset, L, R);
make_inrange(Val, Offset, L, R) when R =/= inf, Val + Offset > R ->
  make_inrange(Val - Offset, L, R);
make_inrange(Val, Offset, L, R) -> make_inrange(Val + Offset, L, R).

%% integers
is_integer_type(Type) ->
  has_same_generator(Type, proper_types:integer()).

integer_gen_sa({'$type', TypeProps}) ->
  {env, {Min, Max}} = proplists:lookup(env, TypeProps),
  fun (Base, null) -> make_inrange(Base, 0, Min, Max);
      (Base, TD) ->
      Temp = ?TEMP(TD),
      OffsetLimit = case Min =:= inf orelse Max =:= inf of
                      true ->
                        trunc(10 * Temp);
                      false ->
                        trunc(abs(Min - Max) * Temp * 0.1) + 1
                    end,
      Offset = proper_arith:rand_int(-OffsetLimit, OffsetLimit),
      make_inrange(Base, Offset, Min, Max)
  end.

%% floats
is_float_type(Type) ->
  has_same_generator(Type, proper_types:float()).

float_gen_sa({'$type', TypeProps}) ->
  {env, {Min, Max}} = proplists:lookup(env, TypeProps),
  fun (Base, null) -> make_inrange(Base, 0, Min, Max);
      (Base, _) ->
      OffsetLimit = case Min =:= inf orelse Max =:= inf of
                      true ->
                        10.0;
                      false ->
                        abs(Min - Max) * 0.001
                    end,
      Offset = proper_arith:rand_float(-OffsetLimit, OffsetLimit),
      make_inrange(Base, Offset, Min, Max)
  end.

%% List
is_list_type(Type) ->
  has_same_generator(Type, proper_types:list(proper_types:atom())).

list_choice(empty, Temp) ->
  C = ?RANDOM_MOD:uniform(),
  C_Add = 0.5 * Temp,
  if
    C < C_Add -> add;
    true      -> nothing
  end;
list_choice({list, GrowthCoefficient}, Temp) ->
  C = ?RANDOM_MOD:uniform(),
  AddCoefficient = 0.3 * GrowthCoefficient,
  DelCoefficient = 0.3 * (1 - GrowthCoefficient),
  C_Add =          AddCoefficient * Temp,
  C_Del = C_Add + (DelCoefficient * Temp),
  C_Mod = C_Del + (0.15 * Temp),
  if
    C < C_Add -> add;
    C < C_Del -> del;
    C < C_Mod -> modify;
    true      -> nothing
  end;
list_choice(vector, Temp) ->
  C = ?RANDOM_MOD:uniform(),
  C_Mod = 0.5 * Temp,
  if
    C < C_Mod -> modify;
    true      -> nothing
  end;
list_choice(tuple, Temp) ->
  list_choice(vector, Temp).

list_gen_sa(Type) ->
  {ok, InternalType} = proper_types:find_prop(internal_type, Type),
  ElementType = replace_generators(InternalType),
  fun (Base, Temp) ->
      GrowthCoefficient = (?RANDOM_MOD:uniform() * 0.8) + 0.1,
      list_gen_internal(Base, Temp, InternalType, ElementType, GrowthCoefficient)
  end.

list_gen_internal([], Temp, InternalType, ElementType, GrowthCoefficient) ->
  %% chance to add an element
  case list_choice(empty, ?TEMP(Temp)) of
    add ->
      {ok, New} = proper_gen:safe_generate(InternalType),
      [New | list_gen_internal([], Temp, InternalType, ElementType, GrowthCoefficient)];
    nothing -> []
  end;
list_gen_internal([H|T], null, InternalType, ElementType, GrowthCoefficient) ->
  [ElementType(H, null) | list_gen_internal(T, null, InternalType, ElementType, GrowthCoefficient)];
list_gen_internal(L=[H|T], Temp, InternalType, ElementType, GrowthCoefficient) ->
  %% chance to modify current element
  %% chance to delete current element
  %% chance to add element in front of current element
  case list_choice({list, GrowthCoefficient}, ?TEMP(Temp)) of
    add ->
      {ok, New} = proper_gen:safe_generate(InternalType),
      [New | list_gen_internal(L, Temp, InternalType, ElementType, GrowthCoefficient)];
    del ->
      list_gen_internal(T, Temp, InternalType, ElementType, GrowthCoefficient);
    modify ->
      [ElementType(H, ?SLTEMP(Temp)) | list_gen_internal(T, Temp, InternalType, ElementType, GrowthCoefficient)];
    nothing ->
      [H | list_gen_internal(T, Temp, InternalType, ElementType, GrowthCoefficient)]
  end.

%% shrink_list
is_shrink_list_type(Type) ->
  has_same_generator(Type, proper_types:shrink_list([])).

shrink_list_gen_sa(Type) ->
  {ok, Env} = proper_types:find_prop(env, Type),
  replace_generators(Env).

%% vector
is_vector_type(Type) ->
  has_same_generator(Type, proper_types:vector(0, undef)).

vector_gen_sa(Type) ->
  {ok, InternalType} = proper_types:find_prop(internal_type, Type),
  {ok, Length} = proper_types:find_prop(env, Type),
  ElementType = replace_generators(InternalType),
  fun (Base, Tem) ->
      GenFunc = fun GEN([], _) ->
                    [];
                    GEN([ '$new' | T], Temp) ->
                    [sample_from_type(ElementType, Temp) | GEN(T, Temp)];
                    GEN([H|T], Temp) ->
                    case list_choice(vector, ?TEMP(Temp)) of
                      modify ->
                        [ElementType(H, Temp) | GEN(T, Temp)];
                      nothing ->
                        case Temp of
                          null ->
                            [ElementType(H, null) | GEN(T, Temp)];
                          _ ->
                            [H | GEN(T, Temp)]
                        end
                    end
                end,
      if
        length(Base) =:= Length -> GenFunc(Base, Tem);
        length(Base) > Length -> GenFunc(lists:sublist(Base, Length), Tem);
        length(Base) < Length ->
          Additional = Length - length(Base),
          GenFunc(Base ++ lists:duplicate(Additional, '$new'), Tem)
      end
  end.

%% atom
is_atom_type(Type) ->
  has_same_generator(Type, proper_types:atom()).

atom_gen_sa(_AtomType) ->
  StringType = proper_types:list(proper_types:integer(0, 255)),
  StringGen = list_gen_sa(StringType),
  fun (Base, Temp) ->
      StringRepr = atom_to_list(Base),
      list_to_atom(StringGen(StringRepr, Temp))
  end.

%% binary
is_binary_type(Type) ->
  has_same_generator(Type, proper_types:binary()).

is_binary_len_type(Type) ->
  has_same_generator(Type, proper_types:binary(1)).

binary_list() ->
  proper_types:list(proper_types:integer(0, 255)).

binary_vector() ->
  proper_types:vector(42, proper_types:integer(0, 255)).

binary_gen_sa(_Type) ->
  ListGen = replace_generators(binary_list()),
  fun (Base, Temp) ->
      ListRepr = binary_to_list(Base),
      list_to_binary(ListGen(ListRepr, ?SLTEMP(Temp)))
  end.

binary_len_gen_sa(_Type) ->
  VectorGen = replace_generators(binary_vector()),
  fun (Base, Temp) ->
      ListRepr = binary_to_list(Base),
      list_to_binary(VectorGen(ListRepr, ?SLTEMP(Temp)))
  end.

%% bitstrings

%% tuples
is_tuple_type(Type) ->
  has_same_generator(Type, proper_types:tuple([undef])).

tuple_gen_sa(Type) ->
  {ok, InternalTuple} = proper_types:find_prop(internal_types, Type),
  InternalTypes = tuple_to_list(InternalTuple),
  ElementGens = [replace_generators(T) || T <- InternalTypes],
  fun ({}, _) -> {};
      (Base, Temp) ->
      ListRepr = tuple_to_list(Base),
      NewTupleAsList = [case list_choice(tuple, ?TEMP(Temp)) of
                          nothing ->
                            case Temp of
                              null -> Gen(Elem, null);
                              _ -> Elem
                            end;
                          modify -> Gen(Elem, Temp)
                        end || {Gen, Elem} <- lists:zip(ElementGens, ListRepr)],
      list_to_tuple(NewTupleAsList)
  end.

%% fixed list
%% needs to handle improper lists!
is_fixed_list_type(Type) ->
  has_same_generator(Type, proper_types:fixed_list([])).

fixed_list_gen_sa(Type) ->
  {ok, InternalTypes} = proper_types:find_prop(internal_types, Type),
  ElementGens = safe_map(fun (E) ->
                             {replace_generators(E), E}
                         end,
                         InternalTypes),
  fun ([], _) -> [];
      (Base, Temp) ->
      {NewFixedList, _} =
        safe_mapfoldl(
          fun ({_, ElementType}, []) ->
              {sample_from_type(ElementType, ?TEMP(Temp)), []};

              ({ElementGen, ElementType}, [B|T]) ->
              case proper_types:is_instance(B, ElementType) of
                true ->
                  NewElement = case list_choice(tuple, ?TEMP(Temp)) of
                                 nothing ->
                                   case Temp of
                                     null ->
                                       ElementGen(B, null);
                                     _ -> B
                                   end;
                                 modify ->
                                   ElementGen(B, ?TEMP(Temp))
                               end,
                  {NewElement, T};
                false ->
                  {sample_from_type(ElementType, ?TEMP(Temp)), [B|T]}
              end;

              ({ElementGen, ElementType}, ImproperTail) ->
              case proper_types:is_instance(ImproperTail, ElementType) of
                true ->
                  NewElement = case list_choice(tuple, ?TEMP(Temp)) of
                                 nothing ->
                                   case Temp of
                                     null ->
                                       ElementGen(ImproperTail, null);
                                     _ -> ImproperTail
                                   end;
                                 modify ->
                                   ElementGen(ImproperTail, ?TEMP(Temp))
                               end,
                  {NewElement, improper_ending};
                false ->
                  {sample_from_type(ElementType, ?TEMP(Temp)), improper_ending}
              end
          end,
          Base,
          ElementGens),
      NewFixedList
  end.

%% union
%% weighted_union
is_union_type(Type) ->
  has_same_generator(Type, proper_types:union([42])) orelse
    has_same_generator(Type, proper_types:weighted_union([{1, 1}])).

union_gen_sa(Type) ->
  {ok, Env} = proper_types:find_prop(env, Type),
  fun (Base, Temp) ->
      %% check if base is of any instance of the
      %% sub elements
      case lists:foldr(fun (E, Acc) ->
                           case proper_types:is_instance(Base, E) of
                             true -> [E|Acc];
                             false -> Acc
                           end
                       end, [], Env) of
        [] ->
          %% generate new
          Index = trunc(?RANDOM_MOD:uniform() * length(Env)) + 1,
          ET = lists:nth(Index, Env),
          {ok, Value} = proper_gen:safe_generate(ET),
          Value;
        PossibleGens  ->
          C = ?RANDOM_MOD:uniform(),
          C_Kep =         0.3 * ?TEMP(Temp),
          C_Chg = C_Kep + 0.3 * ?TEMP(Temp),
          if
            C < C_Kep ->
              %% keep
              Base;
            C < C_Chg ->
              %% change choice
              Index = trunc(?RANDOM_MOD:uniform() * length(Env)) + 1,
              ET = lists:nth(Index, Env),
              {ok, Value} = proper_gen:safe_generate(ET),
              Value;
            true ->
              %% modify amongst the possible
              Index = trunc(?RANDOM_MOD:uniform() * length(PossibleGens)) + 1,
              ElementGen = lists:nth(Index, PossibleGens),
              SAGen = replace_generators(ElementGen),
              SAGen(Base, Temp)
          end
      end
  end.

%% let
is_let_type({'$type', Props}) ->
  constructed =:= proplists:get_value(kind, Props) andalso
    false =:= proplists:get_value(shrink_to_parts, Props);
is_let_type(_) ->
  false.

let_gen_sa(Type) ->
  {ok, Combine} = proper_types:find_prop(combine, Type),
  {ok, PartsType} = proper_types:find_prop(parts_type, Type),
  Matcher = get_matcher(Type),
  PartsGen = replace_generators(PartsType),
  fun (Base, Temp) ->
      LetOuter = case Base of
                   {'$used', Outer, _} -> PartsGen(Outer, ?SLTEMP(Temp));
                   _ -> sample_from_type(PartsType, ?SLTEMP(Temp))
                 end,
      CleanOuter = proper_gen:clean_instance(LetOuter),
      RawCombined = Combine(CleanOuter),
      NewValue = Matcher(Base, RawCombined, Temp),
      {'$used', LetOuter, NewValue}
  end.

get_matcher(Type) ->
  case proper_types:find_prop(matcher, Type) of
    {ok, MatchFun} -> fun (B,I,T) -> MatchFun(B, I, ?TEMP(T)) end;
    error -> fun structural_match/3
  end.

-spec match(term(), proper_types:raw_type(), temperature()) -> term().
match(Base, Type, Temp) ->
  case proper_types:is_type(Type) of
    true ->
      Matcher = get_matcher(Type),
      Matcher(Base, Type, Temp);
    false ->
      %% if we only have values left, we use structural matching
      structural_match(Base, Type, Temp)
  end.

-spec structural_match(term(), proper_types:raw_type(), temperature()) -> term().
structural_match(UncleanBase, UncleanRawType, Temp) ->
  Base = proper_gen:clean_instance(UncleanBase),
  RawType = proper_gen:clean_instance(UncleanRawType),
  case proper_types:is_type(RawType) of
    true ->
      case Base of
        no_matching ->
          sample_from_type(RawType, ?TEMP(Temp));
        _ ->
          Gen = replace_generators(RawType),
          BaseNormalized = Gen(Base, null),
          Gen(BaseNormalized, ?SLTEMP(Temp))
      end;
    false ->
      case sets:is_set(RawType) orelse is_dict(RawType) of
	true -> %% we do not take apart sets and dicts
          sample_from_type(RawType, ?TEMP(Temp));
        false ->
	  if
            is_tuple(RawType) ->
	      RawTypeLst = tuple_to_list(RawType),
              MC = case is_tuple(Base) of
		     true ->
                       structural_match(tuple_to_list(Base), RawTypeLst, Temp);
                     false ->
                       structural_match(no_matching, RawTypeLst, Temp)
                   end,
	      list_to_tuple(MC);
            is_list(RawType) andalso is_list(Base) ->
              case safe_zip(Base, RawType) of
		{ok, ZippedBasesWithTypes} ->
		  per_element_match_cook(ZippedBasesWithTypes, Temp);
		impossible ->
		  sample_from_type(RawType, ?TEMP(Temp))
	      end;
            is_list(RawType) ->
	     %% the base is not matching
	     per_element_match_cook(no_matching_list_zip(RawType), Temp);
            true ->
	     sample_from_type(RawType, ?TEMP(Temp))
	 end
      end
  end.

%% handles improper lists
no_matching_list_zip([]) -> [];
no_matching_list_zip([H|T]) ->[{no_matching, H} | no_matching_list_zip(T)];
no_matching_list_zip(ImproperTail) -> {no_matching, ImproperTail}.

per_element_match_cook(ZippedBasesWithTypes, Temp) ->
  safe_map(fun ({B, RT}) -> structural_match(B, RT, Temp) end, ZippedBasesWithTypes).

safe_map(_Fun, []) -> [];
safe_map(Fun, [H|T]) ->
  [Fun(H) | safe_map(Fun, T)];
safe_map(Fun, ImpT) ->
  Fun(ImpT).

safe_mapfoldl(_, Acc, []) ->
  {[], Acc};
safe_mapfoldl(Fun, Acc, [H|T]) ->
  {NewElement, NewAcc} = Fun(H, Acc),
  {MapReturn, FoldReturn} = safe_mapfoldl(Fun, NewAcc, T),
  {[NewElement | MapReturn], FoldReturn};
safe_mapfoldl(Fun, Acc, ImproperTail) ->
  Fun(ImproperTail, Acc).

safe_zip(L, R) ->
  safe_zip(L, R, []).

safe_zip([], [], Acc) ->
  {ok, lists:reverse(Acc)};
safe_zip([HL | TL], [HR | TR], Acc) ->
  safe_zip(TL, TR, [{HL, HR} | Acc]);
safe_zip([], _, _) ->
  impossible;
safe_zip(_, [], _) ->
  impossible;
safe_zip(ITL, ITR, Acc) ->
  case is_list(ITL) orelse is_list(ITR) of
    true -> impossible;
    _ -> {ok, construct_improper(Acc, {ITL, ITR})}
  end.

construct_improper([], IT) ->
  IT;
construct_improper([H|T], IT) ->
  [H | construct_improper(T, IT)].

%% unsafe check
is_dict({dict, _, _, _, _, _, _, _, _}) -> true;
is_dict(_) -> false.

%% lazy
%% sized
is_wrapper_type(Type) ->
  {ok, wrapper} =:= proper_types:find_prop(kind, Type).

get_cached_size(Type) ->
  Key = erlang:phash2({sized_type, Type}),
  case get(?GEN_NEXT_CACHE) of
    Map when is_map(Map) ->
      case maps:find(Key, Map) of
        error -> not_found;
        Ret -> Ret
      end;
    _ -> not_found
  end.

set_cache_size(Type, Size) ->
  Key = erlang:phash2({sized_type, Type}),
  M = get(?GEN_NEXT_CACHE),
  put(?GEN_NEXT_CACHE, maps:put(Key, Size, M)).

get_size(Type, Temp) ->
  Size = case get_cached_size(Type) of
           not_found ->
             %% use random initial size
             %% proper:get_size(Type);
             trunc(?RANDOM_MOD:uniform() * 21 + 1);
           {ok, Base} ->
             %% alternate base size (max size is not accessible from the generator)
             OffsetLimit = trunc(21 * Temp + 1),
             Offset = trunc(?RANDOM_MOD:uniform() * OffsetLimit + 1),
             make_inrange(Base, Offset, 1, 42)
         end,
  set_cache_size(Type, Size),
  Size.

save_sized_generation(Base, Temp, Next, First) ->
  try
    %% can fail with for example a fixed list
    Next(Base, Temp)
  catch
    error:function_clause ->
      {ok, E} = proper_gen:safe_generate(First),
      E
  end.

wrapper_gen_sa(Type) ->
  case proper_types:get_prop(generator, Type) of
    {typed, Gen} ->
      if
        is_function(Gen, 1) ->
          fun (Base, Temp) ->
              Internal = replace_generators(Gen(Type)),
              Internal(Base, Temp)
          end;
        is_function(Gen, 2) ->
          fun (Base, Temp) ->
              Size = get_size(Type, ?TEMP(Temp)),
              Next = replace_generators(Gen(Type, Size)),
              save_sized_generation(Base, Temp, Next, Type)
          end
      end;
    Gen ->
      if
        is_function(Gen, 0) ->
          fun (Base, Temp) ->
              Internal = replace_generators(Gen()),
              Internal(Base, Temp)
          end;
        is_function(Gen, 1) ->
          fun (Base, Temp) ->
              Size = get_size(Type, ?TEMP(Temp)),
              Next = replace_generators(Gen(Size)),
              save_sized_generation(Base, Temp, Next, Type)
          end
      end
  end.

is_user_defined(Type) ->
  error =/= proper_types:find_prop(user_nf, Type).

user_defined_gen_sa(Type) ->
  NF = proper_types:get_prop(user_nf, Type),
  fun (Base, T) ->
      NewRaw = NF(proper_gen:clean_instance(Base), T),
      {ok, Generated} = proper_gen:safe_generate(NewRaw),
      %% match(Base, NewRaw, T)
      Generated
  end.

%% utility
dont_change(X) ->
  fun (_, _) -> X end.

%%%% -- constructs a neighborhood function `Fun(Base, Temp)' from `Type'
%%-spec get_neighborhood_function(proper_types:type()) -> proper_gen_next:nf().
%%get_neighborhood_function(Type) ->
%%  #{next := Next} = from_proper_generator(Type),
%%  Next.
