%% -*- coding: utf-8 -*-
-module(idna).

%% API
-export([encode/1, encode/2,
         decode/1, decode/2]).

%% compatibility API
-export([to_ascii/1,
         to_unicode/1,
         utf8_to_ascii/1,
         from_ascii/1]).


-export([alabel/1, ulabel/1]).

-export([check_hyphen/1,
         check_nfc/1,
         check_context/1,
         check_initial_combiner/1,
         check_label_length/1]).

-export([check_label/1, check_label/4]).

-define(ACE_PREFIX, "xn--").

-ifdef('OTP_RELEASE').
-define(lower(C), string:lowercase(C)).
-else.
-define(lower(C), string:to_lower(C)).
-endif.

-include("idna_logger.hrl").


-type idna_flags() :: [{uts46, boolean()} |
                       {std3_rules, boolean()} |
                       {transitional, boolean()}].



%% @doc encode Internationalized Domain Names using IDNA protocol
-spec encode(string()) -> string().
encode(Domain) ->
  encode(Domain, []).


%% @doc encode Internationalized Domain Names using IDNA protocol.
%% Input can be mapped to uniccode using [uts46](https://unicode.org/reports/tr46/#Introduction)
%% by setting  the `uts46' flag to `true' (default is `false'). If transition from IDNA 2003 to
%% IDNA 2008 is needed, the flag `transitional' can be set to `true', (default is `false'). If
%% conformance to STD3 is needed, the flag `std3_rules' can be set to `true'. (default is `false').
-spec encode(string(), idna_flags()) -> string().
encode(Domain0, Options) ->
  ok = validate_options(Options),
  Domain = case proplists:get_value(uts46, Options, false) of
             true ->
               STD3Rules = proplists:get_value(std3_rules, Options, false),
               Transitional = proplists:get_value(transitional, Options, false),
               uts46_remap(Domain0, STD3Rules, Transitional);
             false ->
               Domain0
           end,
  Labels = case proplists:get_value(strict, Options, false) of
             false ->
               re:split(Domain, "[.。．｡]", [{return, list}, unicode]);
             true ->
               string:tokens(Domain, ".")
           end,
  case Labels of
    [] -> exit(empty_domain);
    _ ->
      encode_1(Labels, [])
  end.

%% @doc decode an International Domain Name encoded with the IDNA protocol
-spec decode(string()) -> string().
decode(Domain) ->
  decode(Domain, []).

%% @doc decode an International Domain Name encoded with the IDNA protocol
-spec decode(string(), idna_flags()) -> string().
decode(Domain0, Options) ->
  ok = validate_options(Options),
  Domain = case proplists:get_value(uts46, Options, false) of
             true ->
               STD3Rules = proplists:get_value(std3_rules, Options, false),
               Transitional = proplists:get_value(transitional, Options, false),
               uts46_remap(Domain0, STD3Rules, Transitional);
             false ->
               Domain0
           end,

  Labels = case proplists:get_value(strict, Options, false) of
             false ->
               re:split(lowercase(Domain), "[.。．｡]", [{return, list}, unicode]);
             true ->
               string:tokens(lowercase(Domain), ".")
           end,
  case Labels of
    [] -> exit(empty_domain);
    _ ->
      decode_1(Labels, [])
  end.


%% Compatibility API
%%

%% @doc encode an International Domain Name to IDNA protocol (compatibility API)
-spec to_ascii(string()) -> string().
to_ascii(Domain) -> encode(Domain).

%% @doc decode an an encoded International Domain Nam eusing thz IDNA protocol (compatibility API)
-spec to_unicode(string()) -> string().
to_unicode(Domain) -> decode(Domain).


utf8_to_ascii(Domain) ->
  to_ascii(idna_ucs:from_utf8(Domain)).

%% @doc like `to_ascii/1'
-spec from_ascii(nonempty_string()) -> nonempty_string().
from_ascii(Domain) ->
  decode(Domain).


%% Helper functions
%%

validate_options([]) -> ok;
validate_options([uts46|Rs]) -> validate_options(Rs);
validate_options([{uts46, B}|Rs]) when is_boolean(B) -> validate_options(Rs);
validate_options([strict|Rs]) -> validate_options(Rs);
validate_options([{strict, B}|Rs]) when is_boolean(B) -> validate_options(Rs);
validate_options([std3_rules|Rs]) -> validate_options(Rs);
validate_options([{std3_rules, B}|Rs]) when is_boolean(B) -> validate_options(Rs);
validate_options([transitional|Rs]) -> validate_options(Rs);
validate_options([{transitional, B}|Rs]) when is_boolean(B) -> validate_options(Rs);
validate_options([_]) -> erlang:error(badarg).

encode_1([], Acc) ->
  lists:reverse(Acc);
encode_1([Label|Labels], []) ->
  encode_1(Labels, lists:reverse(alabel(Label)));
encode_1([Label|Labels], Acc) ->
  encode_1(Labels, lists:reverse(alabel(Label), [$.|Acc])).

check_nfc(Label) ->
  case characters_to_nfc_list(Label) of
    Label -> ok;
    _ ->
      erlang:exit({bad_label, {nfc, "Label must be in Normalization Form C"}})
  end.

check_hyphen(Label) -> check_hyphen(Label, true).

check_hyphen(Label, true) when length(Label) >= 3 ->
  case lists:nthtail(2, Label) of
    [$-, $-|_] ->
      ErrorMsg = error_msg("Label ~p has disallowed hyphens in 3rd and 4th position", [Label]),
      erlang:exit({bad_label, {hyphen, ErrorMsg}});
    _ ->
      case (lists:nth(1, Label) == $-) orelse (lists:last(Label) == $-) of
        true ->
          ErrorMsg = error_msg("Label ~p must not start or end with a hyphen", [Label]),
          erlang:exit({bad_label, {hyphen, ErrorMsg}});
        false ->
          ok
      end
  end;
check_hyphen(Label, true) ->
  case (lists:nth(1, Label) == $-) orelse (lists:last(Label) == $-) of
    true ->
      ErrorMsg = error_msg("Label ~p must not start or end with a hyphen", [Label]),
      erlang:exit({bad_label, {hyphen, ErrorMsg}});
    false ->
      ok
  end;
check_hyphen(_Label, false) ->
  ok.

check_initial_combiner([CP|_]) ->
  case idna_data:lookup(CP) of
    {[$M|_], _} ->
      erlang:exit({bad_label, {initial_combiner, "Label begins with an illegal combining character"}});
    _ ->
      ok
  end.

check_context(Label) ->
  check_context(Label, Label, true, 0).

check_context(Label, CheckJoiners) ->
  check_context(Label, Label, CheckJoiners, 0).

check_context([CP | Rest], Label, CheckJoiners, Pos) ->
  case idna_table:lookup(CP) of
    'PVALID' ->
      check_context(Rest, Label, CheckJoiners, Pos + 1);
    'CONTEXTJ' ->
        ok =  valid_contextj(CP, Label, Pos, CheckJoiners),
        check_context(Rest, Label, CheckJoiners, Pos + 1);
    'CONTEXTO' ->
      ok =  valid_contexto(CP, Label, Pos, CheckJoiners),
      check_context(Rest, Label, CheckJoiners, Pos + 1);
    _Status ->
      ErrorMsg = error_msg("Codepoint ~p not allowed (~p) at posion ~p in ~p", [CP, _Status, Pos, Label]),
      erlang:exit({bad_label, {context, ErrorMsg}})
  end;
check_context([], _, _, _) ->
  ok.


valid_contextj(CP, Label, Pos, true) ->
  case idna_context:valid_contextj(CP, Label, Pos) of
    true ->
      ok;
    false ->
      ErrorMsg = error_msg("Joiner ~p not allowed at posion ~p in ~p", [CP, Pos, Label]),
      erlang:exit({bad_label, {contextj, ErrorMsg}})
  end;
valid_contextj(_CP, _Label, _Pos, false) ->
  ok.

valid_contexto(CP, Label, Pos, true) ->
  case idna_context:valid_contexto(CP, Label, Pos) of
    true ->
      ok;
    false ->
      ErrorMsg = error_msg("Joiner ~p not allowed at posion ~p in ~p", [CP, Pos, Label]),
      erlang:exit({bad_label, {contexto, ErrorMsg}})
  end;
valid_contexto(_CP, _Label, _Pos, false) ->
  ok.



-spec check_label(string()) -> ok.
check_label(Label) ->
  check_label(Label, true, true, true).

%% @doc validate a label of  a domain
-spec check_label(Label, CheckHyphens, CheckJoiners, CheckBidi) -> Result when
    Label :: string(),
    CheckHyphens :: boolean(),
    CheckJoiners :: boolean(),
    CheckBidi :: boolean(),
    Result :: ok.
check_label(Label, CheckHyphens, CheckJoiners, CheckBidi) ->
  ok = check_nfc(Label),
  ok = check_hyphen(Label, CheckHyphens),
  ok = check_initial_combiner(Label),
  ok = check_context(Label, CheckJoiners),
  ok = check_bidi(Label, CheckBidi),
  ok.


check_bidi(Label, true) ->
  idna_bidi:check_bidi(Label);
check_bidi(_, false) ->
  ok.

check_label_length(Label) when length(Label) > 63 ->
  ErrorMsg = error_msg("The label ~p  is too long", [Label]),
  erlang:exit({bad_label, {too_long, ErrorMsg}});
check_label_length(_) ->
  ok.

alabel(Label0) ->
  Label = case lists:all(fun(C) -> idna_ucs:is_ascii(C) end, Label0) of
            true ->
              _ = try ulabel(Label0)
                  catch
                    _:Error ->
                      ErrorMsg = error_msg("The label ~p  is not a valid A-label: ulabel error=~p", [Label0, Error]),
                      erlang:exit({bad_label, {alabel, ErrorMsg}})
                  end,
              ok = check_label_length(Label0),

              Label0;
            false ->
              ok = check_label(Label0),
              ?ACE_PREFIX ++ punycode:encode(Label0)
          end,
  ok = check_label_length(Label),
  Label.

decode_1([], Acc) ->
  lists:reverse(Acc);
decode_1([Label|Labels], []) ->
  decode_1(Labels, lists:reverse(ulabel(Label)));
decode_1([Label|Labels], Acc) ->
  decode_1(Labels, lists:reverse(ulabel(Label), [$.|Acc])).

ulabel([]) -> [];
ulabel(Label0) ->
  Label = case lists:all(fun(C) -> idna_ucs:is_ascii(C) end, Label0) of
            true ->
              case Label0 of
                [$x,$n,$-,$-|Label1] ->
                  punycode:decode(lowercase(Label1));
                _ ->
                  lowercase(Label0)
              end;
            false ->
              lowercase(Label0)
          end,
  ok = check_label(Label),
  Label.

%% Lowercase all chars in Str
-spec lowercase(String::unicode:chardata()) -> unicode:chardata().
lowercase(CD) when is_list(CD) ->
  try lowercase_list(CD, false)
  catch unchanged -> CD
  end;
lowercase(<<CP1/utf8, Rest/binary>>=Orig) ->
  try lowercase_bin(CP1, Rest, false) of
    List -> unicode:characters_to_binary(List)
  catch unchanged -> Orig
  end;
lowercase(<<>>) ->
  <<>>.


lowercase_list([CP1|[CP2|_]=Cont], _Changed) when $A =< CP1, CP1 =< $Z, CP2 < 256 ->
  [CP1+32|lowercase_list(Cont, true)];
lowercase_list([CP1|[CP2|_]=Cont], Changed) when CP1 < 128, CP2 < 256 ->
  [CP1|lowercase_list(Cont, Changed)];
lowercase_list([], true) ->
  [];
lowercase_list([], false) ->
  throw(unchanged);
lowercase_list(CPs0, Changed) ->
  case unicode_util_compat:lowercase(CPs0) of
    [Char|CPs] when Char =:= hd(CPs0) -> [Char|lowercase_list(CPs, Changed)];
    [Char|CPs] -> append(Char,lowercase_list(CPs, true));
    [] -> lowercase_list([], Changed)
  end.

lowercase_bin(CP1, <<CP2/utf8, Bin/binary>>, _Changed)
  when $A =< CP1, CP1 =< $Z, CP2 < 256 ->
  [CP1+32|lowercase_bin(CP2, Bin, true)];
lowercase_bin(CP1, <<CP2/utf8, Bin/binary>>, Changed)
  when CP1 < 128, CP2 < 256 ->
  [CP1|lowercase_bin(CP2, Bin, Changed)];
lowercase_bin(CP1, Bin, Changed) ->
  case unicode_util_compat:lowercase([CP1|Bin]) of
    [CP1|CPs] ->
      case unicode_util_compat:cp(CPs) of
        [Next|Rest] ->
          [CP1|lowercase_bin(Next, Rest, Changed)];
        [] when Changed ->
          [CP1];
        [] ->
          throw(unchanged)
      end;
    [Char|CPs] ->
      case unicode_util_compat:cp(CPs) of
        [Next|Rest] ->
          [Char|lowercase_bin(Next, Rest, true)];
        [] ->
          [Char]
      end
  end.


append(Char, <<>>) when is_integer(Char) -> [Char];
append(Char, <<>>) when is_list(Char) -> Char;
append(Char, Bin) when is_binary(Bin) -> [Char,Bin];
append(Char, Str) when is_integer(Char) -> [Char|Str];
append(GC, Str) when is_list(GC) -> GC ++ Str.


characters_to_nfc_list(CD) ->
  case unicode_util_compat:nfc(CD) of
    [CPs|Str] when is_list(CPs) -> CPs ++ characters_to_nfc_list(Str);
    [CP|Str] -> [CP|characters_to_nfc_list(Str)];
    [] -> []
  end.


uts46_remap(Str, Std3Rules, Transitional) ->
  characters_to_nfc_list(uts46_remap_1(Str, Std3Rules, Transitional)).

uts46_remap_1([Cp|Rs], Std3Rules, Transitional) ->
  Row = try idna_mapping:uts46_map(Cp)
        catch
          error:badarg  ->
            ?LOG_ERROR("codepoint ~p not found in mapping list~n", [Cp]),
            erlang:exit({invalid_codepoint, Cp})
        end,
  {Status, Replacement} = case Row of
                            {_, _} -> Row;
                            S -> {S, undefined}
                          end,
  if
    (Status =:= 'V');
    ((Status =:= 'D') andalso (Transitional =:= false));
    ((Status =:= '3') andalso (Std3Rules =:= true) andalso (Replacement =:= undefined)) ->
      [Cp] ++ uts46_remap_1(Rs, Std3Rules, Transitional);
    (Replacement =/= undefined) andalso (
        (Status =:= 'M') orelse
          (Status =:= '3' andalso Std3Rules =:= false) orelse
          (Status =:= 'D' andalso Transitional =:= true)) ->
      Replacement ++ uts46_remap_1(Rs, Std3Rules, Transitional);
    (Status =:= 'I') ->
      uts46_remap_1(Rs, Std3Rules, Transitional);
    true ->
      erlang:exit({invalid_codepoint, Cp})
  end;
uts46_remap_1([], _, _) ->
  [].

error_msg(Msg, Fmt) ->
  lists:flatten(io_lib:format(Msg, Fmt)).
