/*******************************************************************************

    Implement a template to keep track of a field references

    Passing field references by `alias` template parameter creates many problem,
    and is extremely cumbersome to work with. Instead, we pass an instance of
    a `FieldRef` around, which also contains structured information.

    Copyright:
        Copyright (c) 2019-2022 BOSAGORA Foundation
        All rights reserved.

    License:
        MIT License. See LICENSE for details.

*******************************************************************************/

module dub.internal.configy.FieldRef;

// Renamed imports as the names exposed by `FieldRef` shadow the imported ones.
import dub.internal.configy.Attributes : CAName = Name, CAOptional = Optional, SetInfo;

import std.meta;
import std.traits;

/*******************************************************************************

    A reference to a field in a `struct`

    The compiler sometimes rejects passing fields by `alias`, or complains about
    missing `this` (meaning it tries to evaluate the value). Sometimes, it also
    discards the UDAs.

    To prevent this from happening, we always pass around a `FieldRef`,
    which wraps the parent struct type (`T`), the name of the field
    as `FieldName`, and other information.

    To avoid any issue, eponymous usage is also avoided, hence the reference
    needs to be accessed using `Ref`.

*******************************************************************************/

package template FieldRef (alias T, string name, bool forceOptional = false)
{
    /// The reference to the field
    public alias Ref = __traits(getMember, T, name);

    /// Type of the field
    public alias Type = typeof(Ref);

    /// The name of the field in the struct itself
    public alias FieldName = name;

    /// The name used in the configuration field (taking `@Name` into account)
    static if (hasUDA!(Ref, CAName))
    {
        static assert (getUDAs!(Ref, CAName).length == 1,
                       "Field `" ~ fullyQualifiedName!(Ref) ~
                       "` cannot have more than one `Name` attribute");

        public immutable Name = getUDAs!(Ref, CAName)[0].name;

        public immutable Pattern = getUDAs!(Ref, CAName)[0].startsWith;
    }
    else
    {
        public immutable Name = FieldName;
        public immutable Pattern = false;
    }

    /// Default value of the field (may or may not be `Type.init`)
    public enum Default = __traits(getMember, T.init, name);

    /// Evaluates to `true` if this field is to be considered optional
    /// (does not need to be present in the YAML document)
    public enum Optional = forceOptional ||
        hasUDA!(Ref, CAOptional) ||
        is(immutable(Type) == immutable(bool)) ||
        is(Type : SetInfo!FT, FT) ||
        (Default != Type.init);
}

unittest
{
    import dub.internal.configy.Attributes : Name;

    static struct Config1
    {
        int integer2 = 42;
        @Name("notStr2")
        @(42) string str2;
    }

    static struct Config2
    {
        Config1 c1dup = { 42, "Hello World" };
        string message = "Something";
    }

    static struct Config3
    {
        Config1 c1;
        int integer;
        string str;
        Config2 c2 = { c1dup: { integer2: 69 } };
    }

    static assert(is(FieldRef!(Config3, "c2").Type == Config2));
    static assert(FieldRef!(Config3, "c2").Default != Config2.init);
    static assert(FieldRef!(Config2, "message").Default == Config2.init.message);
    alias NFR1 = FieldRef!(Config3, "c2");
    alias NFR2 = FieldRef!(NFR1.Ref, "c1dup");
    alias NFR3 = FieldRef!(NFR2.Ref, "integer2");
    alias NFR4 = FieldRef!(NFR2.Ref, "str2");
    static assert(hasUDA!(NFR4.Ref, int));

    static assert(FieldRefTuple!(Config3)[1].Name == "integer");
    static assert(FieldRefTuple!(FieldRefTuple!(Config3)[0].Type)[1].Name == "notStr2");
}

/// A pseudo `FieldRef` used for structs which are not fields (top-level)
package template StructFieldRef (ST, string DefaultName = null)
{
    ///
    public enum Ref = ST.init;

    ///
    public alias Type = ST;

    ///
    public enum Default = ST.init;

    ///
    public enum Optional = false;

    /// Some places reference their parent's Name / FieldName
    public enum Name = DefaultName;
    /// Ditto
    public enum FieldName = DefaultName;
}

/// A pseudo `FieldRef` for nested types (e.g. arrays / associative arrays)
package template NestedFieldRef (ElemT, alias FR)
{
    ///
    public enum Ref = ElemT.init;
    ///
    public alias Type = ElemT;
    ///
    public enum Name = FR.Name;
    ///
    public enum FieldName = FR.FieldName;
    /// Element or keys are never optional
    public enum Optional = false;

}

/// Get a tuple of `FieldRef` from a `struct`
package template FieldRefTuple (T)
{
    static assert(is(T == struct),
                  "Argument " ~ T.stringof ~ " to `FieldRefTuple` should be a `struct`");

    ///
    static if (__traits(getAliasThis, T).length == 0)
        public alias FieldRefTuple = staticMap!(Pred, FieldNameTuple!T);
    else
    {
        /// Tuple of strings of aliased fields
        /// As of DMD v2.100.0, only a single alias this is supported in D.
        private immutable AliasedFieldNames = __traits(getAliasThis, T);
        static assert(AliasedFieldNames.length == 1, "Multiple `alias this` are not supported");

        // Ignore alias to functions (if it's a property we can't do anything)
        static if (isSomeFunction!(__traits(getMember, T, AliasedFieldNames)))
            public alias FieldRefTuple = staticMap!(Pred, FieldNameTuple!T);
        else
        {
            /// "Base" field names minus aliased ones
            private immutable BaseFields = Erase!(AliasedFieldNames, FieldNameTuple!T);
            static assert(BaseFields.length == FieldNameTuple!(T).length - 1);

            public alias FieldRefTuple = AliasSeq!(
                staticMap!(Pred, BaseFields),
                FieldRefTuple!(typeof(__traits(getMember, T, AliasedFieldNames))));
        }
    }

    private alias Pred (string name) = FieldRef!(T, name);
}

/// Returns: An alias sequence of field names, taking UDAs (`@Name` et al) into account
package alias FieldsName (T) = staticMap!(FieldRefToName, FieldRefTuple!T);

/// Helper template for `staticMap` used for strict mode
private enum FieldRefToName (alias FR) = FR.Name;

/// Dub extension
package enum IsPattern (alias FR) = FR.Pattern;
/// Dub extension
package alias Patterns (T) = staticMap!(FieldRefToName, Filter!(IsPattern, FieldRefTuple!T));
