### ----------------------------------------------------
### -- Red
### -- Licenses: Artistic-2.0
### -- Authors: Fernando Correa de Oliveira
### -- File: lib/Red/Traits.rakumod
### ----------------------------------------------------
use Red::Column;
use Red::Attr::Column;
use Red::ResultSeq;
use Red::Phaser;
use MetamodelX::Red::VirtualView;
use MetamodelX::Red::View;
unit module Red::Traits;

=head2 Red::Traits

=head3 is view

#| This trait marks this model as a virtual view
#| it will use the select defined on the `sql` method while using its ResultSeq
multi trait_mod:<is>(Mu:U $model, Bool :$virtual-view! --> Empty) {
    $model.HOW does MetamodelX::Red::VirtualView;
}

#| This trait marks this model as a view
multi trait_mod:<is>(Mu:U $model, Bool :$view! --> Empty) {
    $model.HOW does MetamodelX::Red::View;
}

#| This trait marks this model as a view using the specified name
multi trait_mod:<is>(Mu:U $model, Str :$view! --> Empty) {
    trait_mod:<is>($model, :view);
    trait_mod:<is>($model, :table($view));
}

=head3 is temp

#| This trait marks the corresponding table of the
#| model as TEMPORARY (so it only exists for the time
#| of Red being connected to the database).
multi trait_mod:<is>(Mu:U $model, Bool :$temp! --> Empty) {
    $model.^temp = True;
}

=head3 is rs-class

#| This trait defines the name of the class to be used as
#| ResultSet to this model.
multi trait_mod:<is>(Mu:U $model, Str:D :$rs-class! --> Empty) {
    trait_mod:<is>($model, :rs-class(::($rs-class)))
}

=head3 is rs-class

#| This trait defines the class to be used as ResultSet to this model.
multi trait_mod:<is>(Mu:U $model, Mu:U :$rs-class! --> Empty) {
    die "{$rs-class.^name} should do the Red::ResultSeq role" unless $rs-class ~~ Red::ResultSeq;
    my role RSClass[Mu:U $rclass] { method rs-class(|) { $rclass<> } }
    $model.HOW does RSClass[$rs-class];
}

=head3 is nullable

#| This trait configures all model attributes (columns) to be NULLABLE by default, when used as `is nullable`.
#| Without this trait applied, default for every attribute (column) is NOT NULL,
#| though it can be stated explicitly with writing `is !nullable` for the model.
#| Defaults can be overridden using `is nullable` or `is !nullable` for the attribute (column) itself.
multi trait_mod:<is>(Mu:U $model, Bool :$nullable! --> Empty) {
    $model.HOW does role :: { method default-nullable(|) { $nullable } }
}

=head3 is column

#| Defines the attribute as a column without any custom configuration.
multi trait_mod:<is>(Attribute $attr, Bool :$column! --> Empty) is export {
    trait_mod:<is>($attr, :column{}) if $column
}

=head3 is column

#| Defines the attribute as a column receiving a string to be used as the column name.
multi trait_mod:<is>(Attribute $attr, Str :$column! --> Empty) is export {
    trait_mod:<is>($attr, :column{:name($column)}) if $column
}

=head3 is unique

#| This trait marks an attribute (column) as UNIQUE.
multi trait_mod:<is>(Attribute $attr, Bool :$unique! where $_ == True --> Empty) is export {
    trait_mod:<is>($attr, :column{:unique})
}

=head3 is unique

#| This trait marks an attribute (column) as UNIQUE receiving data to ve used on column definition.
multi trait_mod:<is>(Attribute $attr, :$unique! --> Empty) is export {
    trait_mod:<is>($attr, :column{:$unique})
}

=head3 is id

#| This trait marks an attribute (column) as SQL PRIMARY KEY.
multi trait_mod:<is>(Attribute $attr, Bool :$id! where $_ == True --> Empty) is export {
    trait_mod:<is>($attr, :column{:id, :!nullable})
}

=head3 is serial

#| This trait marks an attribute (column) as SQL PRIMARY KEY with SERIAL data type, which
#| means it auto-increments on each insertion.
multi trait_mod:<is>(Attribute $attr, Bool :$serial! where $_ == True --> Empty) is export {
    trait_mod:<is>($attr, :column{:id, :!nullable, :auto-increment})
}

=head3 is column

multi trait_mod:<is>(Red::Attr::Column $attr, :%column! --> Empty) is export {
    if %column<references>:exists && (%column{<model-name model-type>.none}:exists) {
        die "On Red:api<2> references must declaire :model-name (or :model-type) and the references block must receive the model as reference"
    }
    $attr.args{$_} = %column{$_} for %column.keys
}

#| A generic trait used for customizing a column. It accepts a hash of Bool keys.
#| Possible values include:
#| * id - marks a column PRIMARY KEY
#| * auto-increment - marks a column AUTO INCREMENT
#| * nullable - marks a column as NULLABLE
#| * TBD
multi trait_mod:<is>(Attribute $attr, :%column! --> Empty) is export {
    if %column<references>:exists && (%column{<model-name model-type>.none}:exists) {
        die "On Red:api<2> references must declaire :model-name (or :model-type) and the references block must receive the model as reference"
    }
    $attr does Red::Attr::Column(%column);
}

multi trait_mod:<is>(Attribute $attr, :&referencing! --> Empty) is export {
    die 'On Red:api<2> ":model" is required on "is referencing"'
}

=head3 is referencing

#| Trait that defines a reference receiving a code block, a model name, optional require string and nullable.
multi trait_mod:<is>(Attribute $attr, :$referencing! (&referencing!, Str :$model!, Str :$require = $model, Bool :$nullable = True, *%rest ) --> Empty) is export {
    trait_mod:<is>($attr, :column{ :$nullable, :references(&referencing), model-name  => $model, :$require, |%rest })
}

=head3 is referencing

#| Trait that defines a reference receiving a model name, a column name, and optional require string and nulabble.
multi trait_mod:<is>(Attribute $attr, :$referencing! (Str :$model!, Str :$column!, Str :$require = $model, Bool :$nullable = True, *%rest ) --> Empty) is export {
    trait_mod:<is>($attr, :column{ :$nullable, model-name => $model, column-name => $column, :$require, |%rest })
}


=head3 is referencing

#| Trait that defines a reference receiving a code block, a model type object and an optional nullable.
multi trait_mod:<is>(Attribute $attr, :$referencing! (&referencing!, Mu:U :$model!, Bool :$nullable = True, *%rest ) --> Empty) is export {
    $model.^add_role: Red::Model;
    trait_mod:<is>($attr, :column{ :$nullable, :references(&referencing), model-type  => $model, |%rest })
}

=head3 is referencing

#| Trait that defines a reference receiving a model type object, a column name, and optional nulabble.
multi trait_mod:<is>(Attribute $attr, :$referencing! (Mu:U :$model!, Str :$column!, Bool :$nullable = True, *%rest ) --> Empty) is export {
    trait_mod:<is>($attr, :column{ :$nullable, model-type => $model, column-name => $column, |%rest })
}

=head3 is table

#| This trait allows setting a custom name for a table corresponding to a model.
#| For example, `model MyModel is table<custom_table_name> {}` will use `custom_table_name`
#| as the name of the underlying database table.
multi trait_mod:<is>(Mu:U $model, Str :$table! is copy where .chars > 0 --> Empty) {
    $model.HOW.^attributes.first({ .name eq '$!table' }).set_value($model.HOW, $table)
}

=head3 is database

#| This trait allows setting a custom database to be attached on SQLite
multi trait_mod:<is>(Mu:U $model, Str :$database! is copy where .chars > 0 --> Empty) {
    $model.HOW.^attributes.first({ .name eq '$!database' }).set_value($model.HOW, $database)
}

=head3 is relationship

#| Trait that defines a relationship receiving a code block.
multi trait_mod:<is>(Attribute $attr, :&relationship! --> Empty) is export {
    $attr.package.^add-relationship: $attr, &relationship
}

=head3 is relationship

#| DEPRECATED: Trait that defines a relationship receiving 2 code blocks.
multi trait_mod:<is>(Attribute $attr, :@relationship! where { .all ~~ Callable and .elems == 2 } --> Empty) is DEPRECATED is export {
    $attr.package.^add-relationship: $attr, |@relationship
}

=head3 is relationship

#| Trait that defines a relationship receiving a code block, a model name, and opitionaly require string, optional, no-prefetch and has-one.
multi trait_mod:<is>(
    Attribute $attr,
    :$relationship! (
        &relationship,
        Str  :$model,
        Str  :$require     = $model,
        Bool :$optional,
        Bool :$prefetch = $attr.type !~~ Positional,
        Bool :$no-prefetch = $prefetch.defined && !$prefetch,
        Bool :$has-one
    )
    --> Empty
) is export {
    die "Please, use the has-one experimental feature (use Red <has-one>) to allow using it on relationships"
    	if $has-one && !%Red::experimentals<has-one>;
    $attr.package.^add-relationship: $attr, &relationship, |(:$model with $model), |(:$require with $require), :$optional, :$no-prefetch, |(:$has-one if $has-one)
}

=head3 is relationship

#| Trait that defines a relationship receiving a code block, a model type object, and opitionaly require string, optional, no-prefetch and has-one.
multi trait_mod:<is>(
    Attribute $attr, :$relationship! (
        &relationship,
        Mu:U :$model!,
        Bool :$optional,
        Bool :$prefetch = $attr.type !~~ Positional,
        Bool :$no-prefetch = $prefetch.defined && !$prefetch,
        Bool :$has-one
    )
    --> Empty
) is export {
    die "Please, use the has-one experimental feature (use Red <has-one>) to allow using it on relationships"
        if $has-one && !%Red::experimentals<has-one>;
    $attr.package.^add-relationship: $attr, &relationship, :model-type($model), :$optional, :$no-prefetch, |(:$has-one if $has-one)
}

=head3 is relationship

#| Trait that defines a relationship receiving a column name, a model name and opitionaly a require, optional, no-prefetch and has-one.
multi trait_mod:<is>(
    Attribute $attr, :$relationship! (
        Str  :$column!,
        Str  :$model!,
        Str  :$require = $model,
        Bool :$optional,
        Bool :$prefetch = $attr.type !~~ Positional,
        Bool :$no-prefetch = $prefetch.defined && !$prefetch,
        Bool :$has-one
    )
    --> Empty
) is export {
    die "Please, use the has-one experimental feature (use Red <has-one>) to allow using it on relationships"
    	if $has-one && !%Red::experimentals<has-one>;
    $attr.package.^add-relationship: $attr, :$column, :$model, :$require, :$optional, :$no-prefetch, |(:$has-one if $has-one)
}

multi trait_mod:<is>(
    Attribute $attr,
    Callable :$relationship! (
        @relationship! where *.elems == 2,
        Str :$model!,
        Str :$require = $model,
        Bool :$optional,
        Bool :$prefetch = $attr.type !~~ Positional,
        Bool :$no-prefetch = $prefetch.defined && !$prefetch,
        Bool :$has-one
    )
    --> Empty
) {
    die "Please, use the has-one experimental feature (use Red <has-one>) to allow using it on relationships"
    	if $has-one && !%Red::experimentals<has-one>;
    $attr.package.^add-relationship: $attr, |@relationship, :$model, :$require, :$optional, :$no-prefetch, |(:$has-one if $has-one)
}

=head3 is before-create

#| Trait to define a phaser to run before create a new record
multi trait_mod:<is>(Method $m, :$before-create! --> Empty) {
    $m does BeforeCreate;
}

=head3 is after-create

#| Trait to define a phaser to run after create a new record
multi trait_mod:<is>(Method $m, :$after-create! --> Empty) {
    $m does AfterCreate;
}

=head3 is before-update

#| Trait to define a phaser to run before update a record
multi trait_mod:<is>(Method $m, :$before-update! --> Empty) {
    $m does BeforeUpdate;
}

=head3 is after-update

#| Trait to define a phaser to run after update record
multi trait_mod:<is>(Method $m, :$after-update! --> Empty) {
    $m does AfterUpdate;
}

=head3 is before-delete

#| Trait to define a phaser to run before delete a record
multi trait_mod:<is>(Method $m, :$before-delete! --> Empty) {
    $m does BeforeDelete;
}

=head3 is after-delete

#| Trait to define a phaser to run after delete a record
multi trait_mod:<is>(Method $m, :$after-delete! --> Empty) {
    $m does AfterDelete;
}

=head is sub-module

#| Trait to transform subset into sub-model
multi trait_mod:<is>($subset where { .HOW ~~ Metamodel::SubsetHOW }, Bool :$sub-model) {
    use MetamodelX::Red::SubModelHOW;
    $subset.HOW does MetamodelX::Red::SubModelHOW;
    $subset
}


