src/models/m_rsc.erl

%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2009-2022 Marc Worrell
%% @doc Model for resource data. Interfaces between zotonic, templates and the database.
%% @end

%% Copyright 2009-2022 Marc Worrell
%%
%% 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.

-module(m_rsc).
-author("Marc Worrell <marc@worrell.nl>").

-behaviour(zotonic_model).

-export([
    m_get/3,
    % m_post/3,
    m_delete/3,

    name_to_id/2,
    name_to_id_cat/3,

    page_path_to_id/2,

    get/2,
    get_export/2,
    get_raw/2,
    get_raw_lock/2,
    get_acl_props/2,
    insert/2,
    insert/3,
    delete/2,
    delete/3,
    merge_delete/3,
    merge_delete/4,
    update/3,
    update/4,
    duplicate/3,
    duplicate/4,
    touch/2,

    make_authoritative/2,

    exists/2,

    is_visible/2, is_editable/2, is_deletable/2, is_linkable/2,
    is_me/2,
    is_cat/3,
    is_a/2,
    is_a_id/2,
    is_a/3,

    p/3,
    p/4,
    p_no_acl/3,

    op/2, o/2, o/3, o/4,
    sp/2, s/2, s/3, s/4,
    media/2,
    page_url/2,
    page_url_abs/2,
    rid/2,

    name_lookup/2,
    uri/2,
    uri_lookup/2,
    ensure_name/2,

    common_properties/1
]).

-include_lib("zotonic.hrl").

-type resource() :: resource_id()
                  | list(digits())
                  | resource_name()
                  | resource_uri()
                  | resource_uri_map()
                  | undefined.
-type resource_id() :: integer().
-type resource_name() :: string() | binary() | atom().
-type resource_uri() :: binary().
-type resource_uri_map() :: #{
        % <<"uri">> := resource_uri(),
        % <<"name">> := binary(),
        % <<"is_a">> := [ binary() ],
        binary() => term()
    }.
-type props() :: map().
-type props_legacy() :: proplists:proplist().
-type props_all() :: props() | props_legacy().
-type digits() :: 16#30..16#39.

-type update_function() :: fun(
        ( resource() | insert_rsc, props(), z:context() )
        ->
        {ok, UpdateProps :: props() } | {error, term()}
    ).

-type duplicate_options() :: list(duplicate_option()).
-type duplicate_option() :: edges
                          | {edges, boolean()}
                          | medium
                          | {medium, boolean()}.

-export_type([
    resource/0,
    resource_id/0,
    resource_name/0,
    resource_uri/0,
    props/0,
    props_all/0,
    props_legacy/0,
    update_function/0
]).


%% @doc Fetch the value for the key from a model source
-spec m_get( list(), zotonic_model:opt_msg(), z:context() ) -> zotonic_model:return().
m_get([ Id, <<"is_cat">>, Key | Rest ], _Msg, Context) ->
    {ok, {is_cat(Id, Key, Context), Rest}};
m_get([ Id, <<"is_a">>, Cat | Rest ], _Msg, Context) ->
    IsA = m_rsc:is_a(Id, Cat, Context),
    {ok, {IsA, Rest}};
m_get([ Id, <<"is_a">> ], _Msg, Context) ->
    IsA = m_rsc:is_a(Id, Context),
    {ok, {IsA, []}};
m_get([ Id, Key | Rest ], _Msg, Context) ->
    {ok, {p(Id, Key, Context), Rest}};
m_get([ Id ], _Msg, Context) ->
    case get_export(Id, Context) of
        {ok, Rsc} ->
            {ok, {Rsc, []}};
        {error, _} = Error ->
            Error
    end;
m_get(_Vs, _Msg, _Context) ->
    {error, unknown_path}.


m_delete([ Id ], _Msg, Context) ->
    delete(Id, Context).


%% @doc Return the id of the resource with the name
-spec name_to_id(resource_name(), z:context()) -> {ok, resource_id()} | {error, {unknown_rsc, resource_name()}}.
name_to_id(Name, _Context) when is_integer(Name) ->
    {ok, Name};
name_to_id(undefined, _Context) ->
    {error, {unknown_rsc, undefined}};
name_to_id(<<>>, _Context) ->
    {error, {unknown_rsc, <<>>}};
name_to_id("", _Context) ->
    {error, {unknown_rsc, <<>>}};
name_to_id(Name, Context) ->
    case name_lookup(Name, Context) of
        Id when is_integer(Id) -> {ok, Id};
        _ -> {error, {unknown_rsc, Name}}
    end.

-spec name_to_id_cat(resource(), resource_name(), z:context()) ->
        {ok, resource_id()} | {error, {unknown_rsc_cat, resource(), resource_name()}}.
name_to_id_cat(Name, Cat, Context) when is_integer(Name) ->
    F = fun() ->
        {ok, CatId} = m_category:name_to_id(Cat, Context),
        case z_db:q1("select id from rsc where id = $1 and category_id = $2", [Name, CatId], Context) of
            undefined -> {error, {unknown_rsc_cat, Name, Cat}};
            Id -> {ok, Id}
        end
    end,
    z_depcache:memo(F, {rsc_name, Name, Cat}, ?DAY, [Cat], Context);
name_to_id_cat(Name, Cat, Context) ->
    F = fun() ->
        {ok, CatId} = m_category:name_to_id(Cat, Context),
        case z_db:q1("select id from rsc where Name = $1 and category_id = $2", [Name, CatId], Context) of
            undefined -> {error, {unknown_rsc_cat, Name, Cat}};
            Id -> {ok, Id}
        end
    end,
    z_depcache:memo(F, {rsc_name, Name, Cat}, ?DAY, [Cat], Context).

%% @doc Given a page path, return {ok, Id} with the id of the found
%% resource. When the resource does not have the page path, but did so
%% once, this function will return {redirect, Id} to indicate that the
%% page path was found but is no longer the current page path for the
%% resource.
-spec page_path_to_id( binary() | string(), z:context() ) ->
              {ok, resource_id()}
            | {redirect, resource_id()}
            | {error, {unknown_page_path, binary()}}
            | {error, {illegal_page_path, binary(), length|unicode}}.
page_path_to_id(Path, Context) ->
    Path1 = iolist_to_binary([ $/, z_string:trim(Path, $/) ]),
    case is_utf8(Path1) of
        true when size(Path1) < 200 ->
            case z_db:q1("select id from rsc where page_path = $1", [Path1], Context) of
                undefined ->
                    case z_db:q1(
                        "select id from rsc_page_path_log where page_path = $1",
                        [Path1],
                        Context)
                    of
                        OtherId when is_integer(OtherId) ->
                            {redirect, OtherId};
                        undefined ->
                            {error, {unknown_page_path, Path1}}
                    end;
                Id ->
                    {ok, Id}
            end;
        true ->
            {error, {illegal_page_path, Path1, length}};
        false ->
            {error, {illegal_page_path, Path1, unicode}}
    end.

is_utf8(<<>>) -> true;
is_utf8(<<_/utf8, S/binary>>) -> is_utf8(S);
is_utf8(_) -> false.


%% @doc Get all properties of a resource for export. This adds the
%% page urls for all languages and the language neutral uri to the
%% resource properties. Note that normally the page_url is a single
%% property without translation, as it is generated using the current
%% language context. As this export is used for UIs, all language
%% variants are added.
-spec get_export(Id, Context) -> {ok, Rsc} | {error, Reason} when
    Id :: resource(),
    Context :: z:context(),
    Rsc :: props(),
    Reason :: eacces | enoent.
get_export(Id, Context) ->
    RscId = m_rsc:rid(Id, Context),
    case get(RscId, Context) of
        undefined ->
            case m_rsc:exists(RscId, Context) of
                true ->
                    {error, eacces};
                false ->
                    {error, enoent}
            end;
        Rsc ->
            Rsc1 = add_export_props(RscId, Rsc, Context),
            {ok, Rsc1}
    end.

-spec add_export_props( Id, Rsc, Context ) -> Rsc1 when
    Id :: m_rsc:resource_id(),
    Rsc :: m_rsc:props(),
    Rsc1 :: m_rsc:props(),
    Context :: z:context().
add_export_props(Id, Rsc, Context) ->
    Languages = [ 'x-default' | z_language:enabled_language_codes(Context) ],
    PageUrl = page_url(Id, Context),
    PageUrlAbs = z_context:abs_url(PageUrl, Context),
    PageUrls = lists:map(
        fun(Lang) -> {Lang, page_url(Id, z_context:set_language(Lang, Context))} end,
        Languages),
    PageUrlsAbs = lists:map(
        fun({Lang, Url}) -> {Lang, z_context:abs_url(Url, Context)} end,
        PageUrls),
    ShortUrl = z_dispatcher:url_for(id, [ {id, Id}, {absolute_url, true} ], Context),
    Rsc#{
        <<"uri">> => uri(Id, z_context:set_language('x-default', Context)),
        <<"short_url">> => ShortUrl,
        <<"page_url">> => PageUrl,
        <<"page_url_abs">> => PageUrlAbs,       % canonical url
        <<"alternate_page_url">> => #trans{ tr = PageUrls },
        <<"alternate_page_url_abs">> => #trans{ tr = PageUrlsAbs }
    }.

%% @doc Read a whole resource. Return 'undefined' if the resource was
%%      not found, crash on database errors. The properties are filtered
%%      by the ACL.
-spec get(resource(), z:context()) -> map() | undefined.
get(Id, Context) ->
    case rid(Id, Context) of
        Rid when is_integer(Rid) ->
            case z_acl:rsc_visible(Id, Context) of
                true -> filter_props_acl(Rid, get_cached_unsafe(Rid, Context), Context);
                false -> undefined
            end;
        undefined ->
            undefined
    end.

%% @doc Read a whole resource. Return 'undefined' if the resource was
%%      not found, crash on database errors. The properties are NOT
%%      filtered by the ACL. This is used internally to cache the
%%      resource for all possible users.
-spec get_cached_unsafe(resource_id(), z:context()) -> map() | undefined.
get_cached_unsafe(Id, Context) when is_integer(Id) ->
    z_depcache:memo(
        fun() ->
            % Crash on database errors - we don't want errors to
            % be cached in the depcache.
            case get_raw(Id, false, Context) of
                {ok, Map} ->
                    MapDT = ensure_utc_dates(Map, Context),
                    z_notifier:foldr(#rsc_get{ id = Id }, MapDT, Context);
                {error, nodb} ->
                    undefined;
                {error, enoent} ->
                    undefined
            end
        end,
        Id,
        ?WEEK,
        Context).

-spec filter_props_acl(resource(), map() | undefined, z:context()) -> map() | undefined.
filter_props_acl(_Id, undefined, _Context) ->
    undefined;
filter_props_acl(Id, Props, Context) when is_map(Props) ->
    case z_acl:is_admin(Context) of
        true ->
            Props;
        false ->
            case z_acl:rsc_visible(Id, Context) of
                false ->
                    % Not accessible, only show some primary properties
                    #{
                        <<"id">> => maps:get(<<"id">>, Props),
                        <<"category_id">> => maps:get(<<"category_id">>, Props)
                    };
                true ->
                    % Accessible, filter on property level
                    maps:filter(
                        fun(Property, _Val) ->
                            z_acl:rsc_prop_visible(Id, Property, Context)
                        end,
                        Props)
            end
    end.

%% @doc Get the resource from the database, do not fetch the pivot fields and
%%      do not use the cached result. The properties are NOT filtered by the ACL.
-spec get_raw(resource(), z:context()) -> {ok, map()} | {error, term()}.
get_raw(Id, Context) when is_integer(Id) ->
    get_raw(Id, false, Context).

%% @doc Same as get_raw/2 but also lock the resource for update.
%%      The properties are NOT filtered by the ACL.
-spec get_raw_lock(resource(), z:context()) -> {ok, map()} | {error, term()}.
get_raw_lock(Id, Context) ->
    get_raw(Id, true, Context).

-spec get_raw(resource(), boolean(), z:context()) -> {ok, map()} | {error, term()}.
get_raw(Id, IsLock, Context) when is_integer(Id) ->
    SQL = case z_memo:get(rsc_raw_sql) of
        undefined ->
            AllCols = [ z_convert:to_binary(C) || C <- z_db:column_names(rsc, Context) ],
            DataCols = lists:filter(
                fun (<<"pivot_geocode">>) -> true;
                    (<<"pivot_geocode_qhash">>) -> true;
                    (<<"pivot_location_lat">>) -> true;
                    (<<"pivot_location_lng">>) -> true;
                    (<<"pivot_", _/binary>>) -> false;
                    (_) -> true
                end,
                AllCols),
            Query = iolist_to_binary([
                "select ",lists:join($,, DataCols),
                " from rsc where id = $1"
            ]),
            z_memo:set(rsc_raw_sql, Query),
            Query;
        Memo ->
            Memo
    end,
    SQL1 = case IsLock of
        true -> z_convert:to_list(SQL) ++ " for update";
        false -> z_convert:to_list(SQL)
    end,
    case z_db:qmap_props_row(SQL1, [ Id ], [ {keys, binary} ], Context) of
        {ok, Map} ->
            {ok, map_language_atoms(Map)};
        {error, _} = Error ->
            Error
    end;
get_raw(undefined, _IsLock, _Context) ->
    {error, enoent};
get_raw(Id, IsLock, Context) ->
    get_raw(rid(Id, Context), IsLock, Context).

%% The languages are stored as a psql array, map to the internal atom
%% representation. On update this is mapped to binaries in z_db:update/3.
map_language_atoms(#{ <<"language">> := Lang } = Map) when is_list(Lang) ->
    Lang1 = lists:filtermap(
        fun(Iso) ->
            case z_language:to_language_atom(Iso) of
                {ok, IsoAtom} -> {true, IsoAtom};
                {error, _} -> false
            end
        end,
        Lang),
    Map#{ <<"language">> => Lang1 };
map_language_atoms(Map) ->
    Map#{ <<"language">> => [] }.

%% Fix old records which had serialized data in localtime and no date_is_all_day flag
ensure_utc_dates(#{ <<"tz">> := _ } = Map, _Context) ->
    Map;
ensure_utc_dates(Map, Context) ->
    % Convert dates, assuming the system's default timezone
    DateStart = maps:find(<<"date_start">>, Map),
    DateEnd = maps:find(<<"date_end">>, Map),
    IsAllDay = case {DateStart, DateEnd} of
        {{ok, {_, {0, 0, _StartSec}}}, {ok, {_, {23, 59, _EndSec}}}} ->
            true;
        _ ->
            false
    end,
    Map1 = maps:map(
        fun(K, V) ->
            ensure_utc_date(K, V, IsAllDay)
        end,
        Map),
    Map1#{
        <<"tz">> => z_context:tz(Context),
        <<"date_is_all_day">> => IsAllDay
    }.

% Only date_start, date_end and org_pubdate should be converted
ensure_utc_date(K, Date, IsAllDay) ->
    try
        ensure_utc_date_1(K, Date, IsAllDay)
    catch
        error:badarg ->
            undefined
    end.

ensure_utc_date_1(<<"date_start">>, DT, false) when is_tuple(DT) ->
    hd(calendar:local_time_to_universal_time_dst(DT));
ensure_utc_date_1(<<"date_end">>, DT, false) when is_tuple(DT) ->
    hd(calendar:local_time_to_universal_time_dst(DT));
ensure_utc_date_1(<<"org_pubdate">>, DT, _IsAllDay) when is_tuple(DT) ->
    hd(calendar:local_time_to_universal_time_dst(DT));
ensure_utc_date_1(_K, P, _IsAllDay) ->
    P.


%% @doc Get the ACL fields for the resource with the id.
%% Will always return a valid record, even if the resource does not exist.
-spec get_acl_props(Id :: resource(), z:context()) -> #acl_props{}.
get_acl_props(Id, Context) when is_integer(Id) ->
    F = fun() ->
        Result =
            z_db:q_row(
                "select is_published, is_authoritative, "
                "publication_start, publication_end, "
                "content_group_id, visible_for "
                "from rsc "
                "where id = $1",
                [Id], Context),
        case Result of
            {IsPub, IsAuth, PubS, PubE, CGId, VisFor} ->
                #acl_props{
                    is_published = IsPub,
                    is_authoritative = IsAuth,
                    publication_start = PubS,
                    publication_end = PubE,
                    content_group_id = CGId,
                    visible_for = VisFor
                };
            undefined ->
                #acl_props{
                    is_published = false
                }
        end
    end,
    z_depcache:memo(F, {rsc_acl_fields, Id}, ?DAY, [Id], Context);
get_acl_props(Name, Context) ->
    case rid(Name, Context) of
        undefined ->
            #acl_props{
                is_published = false
            };
        Id -> get_acl_props(Id, Context)
    end.


%% @doc Insert a new resource
-spec insert(props_all(), z:context()) -> {ok, resource_id()} | {error, term()}.
insert(Props, Context) ->
    m_rsc_update:insert(Props, [], Context).

-spec insert(props_all(), list(), z:context()) -> {ok, resource_id()} | {error, term()}.
insert(Props, Options, Context) ->
    m_rsc_update:insert(Props, Options, Context).

%% @doc Delete a resource
-spec delete(resource(), z:context()) -> ok | {error, term()}.
delete(Id, Context) ->
    m_rsc_update:delete(Id, undefined, Context).

-spec delete(resource(), resource(), z:context()) -> ok | {error, term()}.
delete(Id, FollowUp, Context) ->
    m_rsc_update:delete(Id, FollowUp, Context).

%% @doc Merge a resource with another, delete the loser.
-spec merge_delete(resource(), resource(), z:context()) -> ok | {error, term()}.
merge_delete(WinnerId, LoserId, Context) ->
    m_rsc_update:merge_delete(WinnerId, LoserId, [ {is_merge_trans, false} ], Context).

%% @doc Merge a resource with another, delete the loser.
-spec merge_delete(resource(), resource(), list(), z:context()) -> ok | {error, term()}.
merge_delete(WinnerId, LoserId, Options, Context) ->
    m_rsc_update:merge_delete(WinnerId, LoserId, Options, Context).

%% @doc Update a resource
-spec update(
        resource(),
        props_all() | update_function(),
        z:context()
    ) -> {ok, resource()} | {error, term()}.
update(Id, Props, Context) ->
    m_rsc_update:update(Id, Props, Context).

-spec update(
        resource(),
        props_all() | update_function(),
        list(),
        z:context()
    ) -> {ok, resource()} | {error, term()}.
update(Id, Props, Options, Context) ->
    m_rsc_update:update(Id, Props, Options, Context).


%% @doc Duplicate a resource.
-spec duplicate(resource(), props_all(), z:context()) ->
    {ok, NewId :: resource_id()} | {error, Reason :: term()}.
duplicate(Id, Props, Context) ->
    m_rsc_update:duplicate(Id, Props, Context).

-spec duplicate(resource(), props_all(), duplicate_options(), z:context()) ->
    {ok, NewId :: resource_id()} | {error, Reason :: term()}.
duplicate(Id, Props, Options, Context) ->
    m_rsc_update:duplicate(Id, Props, Options, Context).


%% @doc "Touch" the rsc, incrementing the version nr and the modification date/ modifier_id.
%% This should be called as part of another update or transaction and does not resync the caches,
%% and does not check the ACL.  After "touching" the resource will be re-pivoted.
-spec touch(resource(), z:context()) -> {ok, resource_id()} | {error, enoent}.
touch(Id, Context) ->
    case z_db:q(
        "update rsc set version = version + 1, modifier_id = $1, modified = now() where id = $2",
        [z_acl:user(Context), rid(Id, Context)],
        Context
    ) of
        1 ->
            z_depcache:flush(Id, Context),
            IsA = m_rsc:is_a(Id, Context),
            Topic = [ <<"model">>, <<"rsc">>, <<"event">>, Id, <<"update">> ],
            z_mqtt:publish(
                Topic,
                #{
                    id => Id,
                    pre_is_a => IsA,
                    post_is_a => IsA
                },
                Context),
            {ok, Id};
        0 ->
            {error, enoent}
    end.


%% @doc Make a resource authoritative. This removes the uri from the resource and sets the
%% resource as 'authoritative'. The uri is added to the m_rsc_gone delete log.
-spec make_authoritative( m_rsc:resource(), z:context() ) -> {ok, resource_id()} | {error, term()}.
make_authoritative(RscId, Context)  ->
    case m_rsc:rid(RscId, Context) of
        undefined ->
            {error, enoent};
        Id ->
            case m_rsc:p_no_acl(Id, is_authoritative, Context) of
                true ->
                    {error, authoritative};
                false ->
                    case z_acl:rsc_editable(Id, Context) of
                        true ->
                            {ok, _} = m_rsc_gone:gone(Id, Id, Context),
                            NewProps = #{
                                <<"is_authoritative">> => true,
                                <<"uri">> => undefined
                            },
                            m_rsc_update:update(Id, NewProps, Context);
                        false ->
                            {error, eacces}
                    end
            end
    end.


-spec exists(resource(), z:context()) -> boolean().
exists(Id, Context) ->
    case rid(Id, Context) of
        Rid when is_integer(Rid) ->
            case p_no_acl(Rid, <<"id">>, Context) of
                Rid -> true;
                undefined -> false
            end;
        undefined -> false
    end.

-spec is_visible(resource(), z:context()) -> boolean().
is_visible(Id, Context) ->
    z_acl:rsc_visible(Id, Context).

-spec is_editable(resource(), z:context()) -> boolean().
is_editable(Id, Context) ->
    z_acl:rsc_editable(Id, Context).

-spec is_deletable(resource(), z:context()) -> boolean().
is_deletable(Id, Context) ->
    z_acl:rsc_deletable(Id, Context).

-spec is_linkable(resource(), z:context()) -> boolean().
is_linkable(Id, Context) ->
    z_acl:rsc_linkable(Id, Context).

-spec is_me(resource(), z:context()) -> boolean().
is_me(Id, Context) ->
    case rid(Id, Context) of
        RscId when is_integer(RscId) ->
            z_acl:user(Context) =:= RscId;
        _ ->
            false
    end.

is_published_date(Id, Context) ->
    case rid(Id, Context) of
        RscId when is_integer(RscId) ->
            case p_no_acl(RscId, <<"is_published">>, Context) of
                true ->
                    Date = erlang:universaltime(),
                    p_no_acl(RscId, <<"publication_start">>, Context) =< Date
                        andalso p_no_acl(RscId, <<"publication_end">>, Context) >= Date;
                false ->
                    false;
                undefined ->
                    false
            end;
        _ ->
            false
    end.


%% @doc Fetch a property from a resource. When the rsc does not exist, the property does not
%% exist or the user does not have access rights to the property then return 'undefined'.
-spec p(resource(), atom() | binary() | string(), z:context()) -> term() | undefined.
p(_Id, undefined, _Context) ->
    undefined;
p(undefined, _Property, _Context) ->
    undefined;
p(Id, Property, Context) when is_atom(Property) ->
    p(Id, atom_to_binary(Property, utf8), Context);
p(Id, Property, Context) when is_list(Property) ->
    p(Id, unicode:characters_to_binary(Property, utf8), Context);
p(Id, Property, Context)
    when   Property =:= <<"category_id">>
    orelse Property =:= <<"category">>
    orelse Property =:= <<"page_url">>
    orelse Property =:= <<"page_url_abs">>
    orelse Property =:= <<"is_a">>
    orelse Property =:= <<"uri">>
    orelse Property =:= <<"uri_raw">>
    orelse Property =:= <<"is_authoritative">>
    orelse Property =:= <<"is_published">>
    orelse Property =:= <<"exists">>
    orelse Property =:= <<"id">>
    orelse Property =:= <<"privacy">>
    orelse Property =:= <<"default_page_url">> ->
    p_no_acl(rid(Id, Context), Property, Context);
p(Id, Property, Context) when is_binary(Property) ->
    p1(Id, Property, Context).

p1(Id, Property, Context) ->
    case rid(Id, Context) of
        undefined ->
            undefined;
        RId ->
            case z_acl:rsc_visible(RId, Context) of
                true ->
                    case z_acl:rsc_prop_visible(RId, Property, Context) of
                        true -> p_no_acl(RId, Property, Context);
                        false -> undefined
                    end;
                false ->
                    undefined
            end
    end.

%% Fetch property from a resource; but return a default value if not found.
p(Id, Property, DefaultValue, Context) ->
    case p(Id, Property, Context) of
        undefined -> DefaultValue;
        Value -> Value
    end.


%% @doc Fetch a property from a resource, no ACL check is done.
p_no_acl(undefined, _Predicate, _Context) ->
    undefined;
p_no_acl(_Id, undefined, _Context) ->
    undefined;
p_no_acl(Id, Prop, Context) when is_atom(Prop) ->
    p_no_acl(Id, atom_to_binary(Prop, utf8), Context);
p_no_acl(Id, Prop, Context) when not is_integer(Id) ->
    p_no_acl(rid(Id, Context), Prop, Context);
p_no_acl(Id, <<"o">>, Context) -> o(Id, Context);
p_no_acl(Id, <<"s">>, Context) -> s(Id, Context);
p_no_acl(Id, <<"op">>, Context) -> op(Id, Context);
p_no_acl(Id, <<"sp">>, Context) -> sp(Id, Context);
p_no_acl(Id, <<"is_me">>, Context) -> is_me(Id, Context);
p_no_acl(Id, <<"is_visible">>, Context) -> is_visible(Id, Context);
p_no_acl(Id, <<"is_editable">>, Context) -> is_editable(Id, Context);
p_no_acl(Id, <<"is_deletable">>, Context) -> is_deletable(Id, Context);
p_no_acl(Id, <<"is_linkable">>, Context) -> is_linkable(Id, Context);
p_no_acl(Id, <<"is_published_date">>, Context) -> is_published_date(Id, Context);
p_no_acl(Id, <<"is_a">>, Context) -> is_a(Id, Context);
p_no_acl(Id, <<"exists">>, Context) -> exists(Id, Context);
p_no_acl(Id, <<"page_url_abs">>, Context) ->
    case p_no_acl(Id, <<"page_path">>, Context) of
        undefined -> page_url(Id, true, Context);
        PagePath ->
            opt_url_abs(z_notifier:foldl(#url_rewrite{args = [{id, Id}]}, PagePath, Context), true, Context)
    end;
p_no_acl(Id, <<"page_url">>, Context) ->
    case p_no_acl(Id, <<"page_path">>, Context) of
        undefined -> page_url(Id, false, Context);
        PagePath ->
            opt_url_abs(z_notifier:foldl(#url_rewrite{args = [{id, Id}]}, PagePath, Context), false, Context)
    end;
p_no_acl(Id, <<"translation">>, Context) ->
    fun(Code) ->
        fun(Prop) ->
            case p_no_acl(Id, Prop, Context) of
                #trans{} = Translated ->
                    z_trans:lookup(Translated, Code, Context);
                Value -> Value
            end
        end
    end;
p_no_acl(Id, <<"default_page_url">>, Context) -> page_url(Id, Context);
p_no_acl(Id, <<"image_url_abs">>, Context) -> z_context:abs_url(p_no_acl(Id, <<"image_url">>, Context), Context);
p_no_acl(Id, <<"thumbnail_url_abs">>, Context) -> z_context:abs_url(p_no_acl(Id, <<"thumbnail_url">>, Context), Context);
p_no_acl(Id, <<"image_url">>, Context) -> image_url(Id, <<"image">>, Context);
p_no_acl(Id, <<"thumbnail_url">>, Context) -> image_url(Id, <<"thumbnail">>, Context);
p_no_acl(Id, <<"uri">>, Context) -> uri(Id, Context);
p_no_acl(Id, <<"uri_raw">>, Context) -> p_cached(Id, <<"uri">>, Context);
p_no_acl(Id, <<"category">>, Context) -> m_category:get(p_no_acl(Id, <<"category_id">>, Context), Context);
p_no_acl(Id, <<"media">>, Context) -> media(Id, Context);
p_no_acl(Id, <<"medium">>, Context) -> m_media:get(Id, Context);
p_no_acl(Id, <<"depiction">>, Context) -> m_media:depiction(Id, Context);
p_no_acl(Id, <<"predicates_edit">>, Context) -> predicates_edit(Id, Context);
p_no_acl(Id, <<"day_start">>, Context) ->
    case p_cached(Id, <<"date_start">>, Context) of
        {{_, _, _} = Date, _} -> Date;
        _Other -> undefined
    end;
p_no_acl(Id, <<"day_end">>, Context) ->
    case p_cached(Id, <<"date_end">>, Context) of
        {{_, _, _} = Date, _} -> Date;
        _Other -> undefined
    end;
p_no_acl(Id, <<"email_raw">>, Context) ->
    z_html:unescape(p_no_acl(Id, <<"email">>, Context));
% p_no_acl(Id, title, Context) ->
%     Title = p_cached(Id, title, Context),
%     Title1 = case z_utils:is_empty(Title) of true -> undefined; false -> Title end,
%     case z_notifier:first(#rsc_property{id=Id, property=title, value=Title1}, Context) of
%         undefined -> Title;
%         OtherTitle -> OtherTitle
%     end;
p_no_acl(Id, <<"title_slug">>, Context) ->
    case p_cached(Id, <<"title_slug">>, Context) of
        undefined -> p_cached(Id, <<"slug">>, Context);
        Slug -> Slug
    end;

% Check if the requested predicate is a readily available property or an edge
p_no_acl(Id, Predicate, Context) when is_integer(Id) ->
    p_cached(Id, Predicate, Context).


p_cached(Id, Property, Context) ->
    case p_cached_1(Id, Property, Context) of
        undefined ->
            case z_rdf_props:mapping(Property) of
                undefined ->
                    undefined;
                MappedProp ->
                    p_cached_1(Id, MappedProp, Context)
            end;
        V ->
            V
    end.

p_cached_1(Id, Property, Context) ->
    case z_depcache:get(Id, Property, Context) of
        {ok, V} ->
            V;
        undefined ->
            case get_cached_unsafe(Id, Context) of
                undefined -> undefined;
                Map ->
                    maps:get(Property, Map, undefined)
            end
    end.


%% @doc Determine the non informational uri of a resource.
-spec uri( resource() | undefined, z:context() ) -> binary() | undefined.
uri(Id, Context) when is_integer(Id) ->
    case p_cached(Id, <<"uri">>, Context) of
        Empty when Empty =:= <<>>; Empty =:= undefined ->
            uri_dispatch(Id, Context);
        Uri ->
            Uri
    end;
uri(undefined, _Context) ->
    undefined;
uri(Id, Context) ->
    uri(rid(Id, Context), Context).

uri_dispatch(Id, Context) ->
    DispatchId = case is_named_meta(Id, Context) of
        {true, Name} -> Name;
        false -> Id
    end,
    case z_dispatcher:url_for(id, [{id, DispatchId}], z_context:set_language(undefined, Context)) of
        undefined ->
            iolist_to_binary(z_context:abs_url(<<"/id/", (z_convert:to_binary(DispatchId))/binary>>, Context));
        Url ->
            iolist_to_binary(z_context:abs_url(Url, Context))
    end.

is_named_meta(Id, Context) ->
    case p_cached(Id, <<"name">>, Context) of
        Empty when Empty =:= <<>>; Empty =:= undefined ->
            false;
        Name ->
            case is_a(Id, meta, Context) of
                true ->
                    {true, Name};
                false ->
                    false
            end
    end.

image_url(Id, Mediaclass, Context) ->
    case z_media_tag:url(Id, [ {mediaclass, Mediaclass} ], Context) of
        {ok, <<>>} ->
            undefined;
        {ok, Url} ->
            Url;
        {error, _} ->
            undefined
    end.


%% Return a list of all edge predicates of this resource
-spec op(resource(), z:context()) -> list().
op(Id, Context) when is_integer(Id) ->
    m_edge:object_predicates(Id, Context);
op(undefined, _Context) ->
    [];
op(Id, Context) ->
    op(rid(Id, Context), Context).

%% @doc Used for dereferencing object edges inside template expressions
-spec o(resource(), z:context()) -> fun().
o(Id, _Context) ->
    fun(P, Context) -> o(Id, P, Context) end.

%% @doc Return the list of objects with a certain predicate
-spec o(resource(), atom(), z:context()) -> list().
o(undefined, _Predicate, _Context) ->
    [];
o(_Id, undefined, _Context) ->
    [];
o(Id, Predicate, Context) when is_integer(Id) ->
    m_edge:objects(Id, Predicate, Context);
o(Id, Predicate, Context) ->
    o(rid(Id, Context), Predicate, Context).


%% Return the nth object in the predicate list
-spec o(resource(), atom(), pos_integer(), z:context()) -> resource_id() | undefined.
o(_Id, undefined, _N, _Context) ->
    undefined;
o(undefined, _Predicate, _N, _Context) ->
    undefined;
o(Id, Predicate, N, Context) when is_integer(Id) ->
    case m_edge:object(Id, Predicate, N, Context) of
        undefined -> undefined;
        ObjectId -> ObjectId
    end;
o(Id, Predicate, N, Context) ->
    o(rid(Id, Context), Predicate, N, Context).


%% Return a list of all edge predicates to this resource
-spec sp(resource(), z:context()) -> list().
sp(undefined, _Context) ->
    [];
sp(Id, Context) when is_integer(Id) ->
    m_edge:subject_predicates(Id, Context);
sp(Id, Context) ->
    sp(rid(Id, Context), Context).

%% Used for dereferencing subject edges inside template expressions
-spec s(resource(), z:context()) -> fun().
s(Id, _Context) ->
    fun(P, Context) -> s(Id, P, Context) end.

%% Return the list of subjects with a certain predicate
-spec s(resource(), atom(), z:context()) -> list().
s(undefined, _Predicate, _Context) ->
    [];
s(_Id, undefined, _Context) ->
    [];
s(Id, Predicate, Context) when is_integer(Id) ->
    m_edge:subjects(Id, Predicate, Context);
s(Id, Predicate, Context) ->
    s(rid(Id, Context), Predicate, Context).

%% Return the nth object in the predicate list
-spec s(resource(), atom(), pos_integer(), z:context()) -> resource_id() | undefined.
s(undefined, _Predicate, _N, _Context) ->
    undefined;
s(_Id, undefined, _N, _Context) ->
    undefined;
s(Id, Predicate, N, Context) when is_integer(Id) ->
    case m_edge:subject(Id, Predicate, N, Context) of
        undefined -> undefined;
        SubjectId -> SubjectId
    end;
s(Id, Predicate, N, Context) ->
    s(rid(Id, Context), Predicate, N, Context).


%% Return the list of all media attached to the resource
-spec media(resource(), z:context()) -> list().
media(Id, Context) when is_integer(Id) ->
    m_edge:objects(Id, depiction, Context);
media(undefined, _Context) ->
    [];
media(Id, Context) ->
    media(rid(Id, Context), Context).


%% @doc Fetch a resource id from any input
-spec rid(resource()|#trans{}, z:context()) -> resource_id() | undefined.
rid(Id, _Context) when is_integer(Id) ->
    Id;
rid({Id}, _Context) when is_integer(Id) ->
    Id;
rid(#rsc_list{list = [R | _]}, _Context) ->
    R;
rid(#rsc_list{list = []}, _Context) ->
    undefined;
rid(undefined, _Context) ->
    undefined;
rid(<<>>, _Context) ->
    undefined;
rid([], _Context) ->
    undefined;
rid([X|_], _Context) when not is_integer(X) ->
    undefined;
rid(#trans{} = Tr, Context) ->
    rid(z_trans:lookup_fallback(Tr, Context), Context);
rid(<<"urn:", _/binary>> = Uri, Context) ->
    uri_lookup(Uri, Context);
rid(<<"http:", _/binary>> = Uri, Context) ->
    uri_lookup(Uri, Context);
rid(<<"https:", _/binary>> = Uri, Context) ->
    uri_lookup(Uri, Context);
rid(<<"/", _/binary>> = Uri, Context) ->
    uri_lookup(Uri, Context);
rid("urn:" ++ _ = Uri, Context) ->
    uri_lookup(Uri, Context);
rid("http:" ++ _ = Uri, Context) ->
    uri_lookup(Uri, Context);
rid("https:" ++ _ = Uri, Context) ->
    uri_lookup(Uri, Context);
rid("/" ++ _ = Uri, Context) ->
    uri_lookup(Uri, Context);
rid(#{ <<"uri">> := Uri } = Map, Context) ->
    Name = maps:get(<<"name">>, Map, undefined),
    case rid(Uri, Context) of
        undefined ->
            case rid(Name, Context) of
                undefined ->
                    undefined;
                Id ->
                    case is_matching_category(maps:get(<<"is_a">>, Map, undefined), is_a(Id, Context)) of
                        true -> Id;
                        false -> undefined
                    end
            end;
        Id ->
            Id
    end;
rid(#{ <<"@id">> := Uri }, Context) ->
    uri_lookup(Uri, Context);
rid(MaybeName, Context) when is_binary(MaybeName) ->
    case z_utils:only_digits(MaybeName) of
        true ->
            z_convert:to_integer(MaybeName);
        false ->
            case binary:match(MaybeName, <<":">>) of
                nomatch -> name_lookup(MaybeName, Context);
                _ -> uri_lookup(MaybeName, Context)
            end
    end;
rid(MaybeName, Context) when is_list(MaybeName) ->
    case z_utils:only_digits(MaybeName) of
        true ->
            z_convert:to_integer(MaybeName);
        false ->
            case lists:any(fun(C) -> C =:= $: end, MaybeName) of
                false -> name_lookup(MaybeName, Context);
                true -> uri_lookup(MaybeName, Context)
            end
    end;
rid(MaybeName, Context) ->
    name_lookup(MaybeName, Context).

is_matching_category(undefined, _) -> true;
is_matching_category([], _) -> true;
is_matching_category(ExtIsA, LocalIsA) ->
    ExtIsA1 = [ z_convert:to_binary(A) || A <- ExtIsA ],
    LocalIsA1 = [ z_convert:to_binary(A) || A <- LocalIsA ],
    lists:any( fun(A) -> lists:member(A, ExtIsA1) end, LocalIsA1 ).


%% @doc Return the id of the resource with a certain unique name.
-spec name_lookup(resource_name(), z:context()) -> resource_id() | undefined.
name_lookup(Name, Context) ->
    try
        z_string:to_name(Name)
    of
        Lower ->
            case z_depcache:get({rsc_name, Lower}, Context) of
                {ok, undefined} ->
                    undefined;
                {ok, Id} ->
                    Id;
                undefined ->
                    Id = case z_db:q1("select id from rsc where name = $1", [Lower], Context) of
                        undefined -> undefined;
                        Value -> Value
                    end,
                    z_depcache:set({rsc_name, Lower}, Id, ?DAY, [Id, {rsc_name, Lower}], Context),
                    Id
            end
    catch
        error:badarg ->
            undefined
    end.


%% @doc Return the id of the resource with a certain uri.
-spec uri_lookup( resource_uri() | string(), z:context()) -> resource_id() | undefined.
uri_lookup(<<>>, _Context) ->
    undefined;
uri_lookup(Uri, Context) when is_binary(Uri) ->
    case is_rsc_uri(Uri) of
        true ->
            case is_local_uri(Uri, Context) of
                true ->
                    % Check for id in URL
                    local_uri_to_id(Uri, Context);
                false ->
                    case z_depcache:get({rsc_uri, Uri}, Context) of
                        {ok, undefined} ->
                            undefined;
                        {ok, Id} ->
                            Id;
                        undefined ->
                            Id = uri_lookup_1(Uri, Context),
                            z_depcache:set({rsc_uri, Uri}, Id, ?DAY, [Id, {rsc_uri, Uri}], Context),
                            Id
                    end
            end;
        false ->
            undefined
    end;
uri_lookup(Uri, Context) ->
    uri_lookup(z_convert:to_binary(Uri), Context).

% Check if URI is imported or replaced by another resource
uri_lookup_1(Uri, Context) ->
    case z_db:q1("select id from rsc where uri = $1", [Uri], Context) of
        undefined ->
            case m_rsc_gone:get_uri(Uri, Context) of
                undefined -> undefined;
                Gone -> proplists:get_value(new_id, Gone)
            end;
        Id ->
            Id
    end.


%% @doc Check if the hostname in an URL matches the current site
is_local_uri(<<"/", C, _/binary>>, _Context) when C =/= $/ ->
    true;
is_local_uri(Uri, Context) ->
    Site = z_context:site(Context),
    case z_sites_dispatcher:get_site_for_url(Uri) of
        {ok, Site} ->
            true;
        _ ->
            % Unknown or some other site
            false
    end.


%% @doc Use the dispatcher to extract the id from the local URI
local_uri_to_id(<<$/, C, _/binary>> = Path, Context) when C =/= $/ ->
    case z_sites_dispatcher:dispatch_path(Path, Context) of
        {ok, #{
            controller_options := Options,
            bindings := Bindings
        }} ->
            Id = maps:get(id, Bindings, proplists:get_value(id, Options)),
            rid(Id, Context);
        _ ->
            % Non matching sites and illegal urls are rejected
            undefined
    end;
local_uri_to_id(Uri, Context) ->
    Site = z_context:site(Context),
    case z_sites_dispatcher:dispatch_url(Uri) of
        {ok, #{
            site := Site,
            controller_options := Options,
            bindings := Bindings
        }} ->
            Id = maps:get(id, Bindings, proplists:get_value(id, Options)),
            rid(Id, Context);
        _ ->
            % Non matching sites and illegal urls are rejected
            undefined
    end.

is_rsc_uri(<<"urn:", _/binary>>) -> true;
is_rsc_uri(<<"http:", _/binary>>) -> true;
is_rsc_uri(<<"https:", _/binary>>) -> true;
is_rsc_uri(<<"/", _/binary>>) -> true;
is_rsc_uri(B) ->
    binary:match(B, <<":">>) =/= nomatch.


%% @doc Check if the resource is exactly the category
-spec is_cat(resource(), atom(), z:context()) -> boolean().
is_cat(Id, Cat, Context) ->
    case m_category:name_to_id(Cat, Context) of
        {ok, CatId} ->
            RscCatId = p(Id, <<"category_id">>, Context),
            case RscCatId of
                CatId ->
                    true;
                _ ->
                    Path = m_category:get_path(RscCatId, Context),
                    lists:any(fun(X) -> X == CatId end, Path)
            end;
        _ ->
            false
    end.

%% @doc Return the categories and the inherited categories of the resource. Returns a list with category atoms
-spec is_a(resource(), z:context()) -> list(atom()).
is_a(Id, Context) ->
    RscCatId = p(Id, category_id, Context),
    m_category:is_a(RscCatId, Context).

%% @doc Return the categories and the inherited categories of the resource. Returns a list with
%% category ids
-spec is_a_id(resource(), z:context()) -> list(pos_integer()).
is_a_id(Id, Context) ->
    RscCatId = p(Id, <<"category_id">>, Context),
    [RscCatId | m_category:get_path(RscCatId, Context)].

%% @doc Check if the resource is in a category.
-spec is_a(resource(), m_category:category(), z:context()) -> boolean().
is_a(Id, Cat, Context) ->
    RscCatId = p(Id, <<"category_id">>, Context),
    m_category:is_a(RscCatId, Cat, Context).

-spec page_url( resource(), z:context() ) -> iodata() | undefined.
page_url(Id, Context) ->
    page_url(Id, false, Context).

-spec page_url_abs( resource(), z:context() ) -> iodata() | undefined.
page_url_abs(Id, Context) ->
    page_url(Id, true, Context).

-spec page_url( resource(), boolean(), z:context() ) -> iodata() | undefined.
page_url(Id, IsAbs, Context) ->
    case rid(Id, Context) of
        RscId when is_integer(RscId) ->
            CatPath = lists:reverse(is_a(Id, Context)),
            case z_notifier:first(#page_url{id = RscId, is_a = CatPath}, Context) of
                {ok, Url} ->
                    opt_url_abs(Url, IsAbs, Context);
                undefined ->
                    Args = [
                        {id,RscId},
                        {slug, slug(RscId, Context)}
                        | z_context:get(extra_args, Context, [])
                    ],
                    Url = page_url_path(CatPath, Args, Context),
                    case IsAbs of
                        true -> z_dispatcher:abs_url(Url, Context);
                        false -> Url
                    end
            end;
        _ ->
            undefined
    end.

page_url_path([], Args, Context) ->
    case z_dispatcher:url_for(page, Args, Context) of
        undefined ->
            ?LOG_WARNING("Failed to get page url path. Is the 'page' dispatch rule missing?"),
            undefined;
        Url -> Url
    end;
page_url_path([CatName | Rest], Args, Context) ->
    case z_dispatcher:url_for(CatName, Args, Context) of
        undefined -> page_url_path(Rest, Args, Context);
        Url -> Url
    end.

slug(Id, Context) ->
    case p(Id, <<"title_slug">>, Context) of
        undefined -> undefined;
        <<>> -> undefined;
        #trans{} = Tr -> z_trans:lookup_fallback(Tr, Context);
        Slug when is_binary(Slug) -> Slug
    end.

%% @doc Depending on the context or the requested property we make the URL absolute
opt_url_abs(undefined, _IsAbs, _Context) ->
    undefined;
opt_url_abs(Url, true, Context) ->
    z_dispatcher:abs_url(Url, Context);
opt_url_abs(Url, false, Context) ->
    case z_context:get(absolute_url, Context) of
        true -> z_dispatcher:abs_url(Url, Context);
        _ -> Url
    end.

%% @doc Return the predicates that are valid combined with the predicates that
%% are actually used by the subject.
%% This list is to show which predicates are editable for the subject rsc.
%% @spec predicates_edit(Id, Context) -> [Predicate]
predicates_edit(Id, Context) ->
    ByCategory = m_predicate:for_subject(Id, Context),
    Present = m_edge:object_predicate_ids(Id, Context),
    ByCategory ++ Present.


%% @doc Ensure that a resource has a name, caller must have update rights.
-spec ensure_name(integer(), z:context()) -> ok.
ensure_name(Id, Context) ->
    case p_no_acl(Id, <<"name">>, Context) of
        undefined ->
            CatId = p_no_acl(Id, <<"category_id">>, Context),
            CatName = p_no_acl(CatId, <<"name">>, Context),
            BaseName = z_string:to_name(iolist_to_binary([CatName, $_, english_title(Id, Context)])),
            BaseName1 = ensure_name_maxlength(BaseName),
            Name = ensure_name_unique(BaseName1, 0, Context),
            {ok, _} = m_rsc_update:update(Id, [{<<"name">>, Name}], z_acl:sudo(Context)),
            ok;
        _Name ->
            ok
    end.

ensure_name_maxlength(<<Name:70/binary, _/binary>>) -> Name;
ensure_name_maxlength(Name) -> Name.

english_title(Id, Context) ->
    case p_no_acl(Id, <<"title">>, Context) of
        Title when is_binary(Title) -> Title;
        #trans{ tr=[] } -> <<>>;
        undefined -> <<>>;
        #trans{ tr=Tr } ->
            case proplists:get_value(en, Tr) of
                undefined ->
                    {_, T} = hd(Tr),
                    T;
                T ->
                    T
            end
    end.

ensure_name_unique(BaseName, N, Context) ->
    Name = iolist_to_binary([BaseName, postfix(N)]),
    case z_db:q1("select id from rsc where name = $1", [Name], Context) of
        undefined -> Name;
        _Id -> ensure_name_unique(BaseName, N + 1, Context)
    end.

postfix(0) -> <<>>;
postfix(N) -> integer_to_list(N).


%% @doc Common properties, these are used by exporter and backup routines.
common_properties(_Context) ->
    [
        <<"title">>,

        <<"category_id">>,
        <<"creator_id">>,
        <<"modifier_id">>,

        <<"created">>,
        <<"modified">>,

        <<"publication_start">>,
        <<"publication_end">>,

        <<"is_published">>,
        <<"is_featured">>,
        <<"is_protected">>,

        <<"chapeau">>,
        <<"subtitle">>,
        <<"short_title">>,
        <<"summary">>,

        <<"name_prefix">>,
        <<"name_first">>,
        <<"name_surname_prefix">>,
        <<"name_surname">>,

        <<"phone">>,
        <<"phone_mobile">>,
        <<"phone_alt">>,
        <<"phone_emergency">>,

        <<"email">>,
        <<"website">>,

        <<"date_start">>,
        <<"date_end">>,
        <<"date_remarks">>,

        <<"address_street_1">>,
        <<"address_street_2">>,
        <<"address_city">>,
        <<"address_state">>,
        <<"address_postcode">>,
        <<"address_country">>,

        <<"mail_street_1">>,
        <<"mail_street_2">>,
        <<"mail_city">>,
        <<"mail_state">>,
        <<"mail_postcode">>,
        <<"mail_country">>,

        <<"location_lng">>,
        <<"location_lat">>,

        <<"body">>,
        <<"body_extra">>,
        <<"blocks">>,

        <<"page_path">>,
        <<"name">>,

        <<"seo_noindex">>,
        <<"title_slug">>,
        <<"custom_slug">>,
        <<"seo_desc">>
    ].