### ----------------------------------------------------
### -- Red
### -- Licenses: Artistic-2.0
### -- Authors: Fernando Correa de Oliveira
### -- File: lib/Red/Column.rakumod
### ----------------------------------------------------
use Red::Utils;
use Red::Model;
use Red::AST;
use Red::AST::Unary;
use Red::AST::IsDefined;
use Red::Formatter;

=head2 Red::Column

#| Represents a database column
unit class Red::Column does Red::AST;
also does Red::Formatter;

sub inflator($attr) {
    do with $attr.type.?inflator {
        $_
    } else {
        do if $attr.type ~~ Enumeration {
            -> $value, $type { $type.($value) }
        } else {
            -> $_ { .?"{ $attr.type.^name }"() // .self }
        }
    }
}
sub deflator($_) {
    do with .type.?deflator {
        $_
    } else {
        do if .type ~~ Enumeration {
            *.value
        } else {
            *.self
        }
    }
}

has Attribute   $.attr is required;
has             $.model            = $!attr.package;
has Str         $.attr-name        = $!attr.name.substr: 2;
has Bool        $.id               = False;
has Bool        $.auto-increment   = False;
has             &.references;
has             &!actual-references;
has             $!ref;
has Bool        $.nullable         = $!attr.package.HOW.?default-nullable($!attr.package) // False;
has Str         $.name             = ::?CLASS.column-formatter: self.attr.name.substr: 2;
has Str         $.name-alias       = $!name;
has Str         $.type;
has             &.inflate          = $!attr.&inflator;
has             &.deflate          = $!attr.&deflator;
has             $.computation;
has Str         $.model-name;
has Red::Model  $.model-type;
has Str         $.column-name;
has Str         $.require          = $!model-name;
has Mu          $.class            = $!attr.package;
has             @.unique-groups;
has             &.default;

#multi method WHICH(::?CLASS:D:) {
#    ValueObjAt.new: self.^name ~ "|" ~ self.migration-hash.pairs.sort.map(-> (:$key, :$value) {
#        "$key|$value"
#    }).join: "|"
#}

multi method WHICH(::?CLASS:D:) {
    ValueObjAt.new: self.gist
}

multi method perl(::?CLASS:D:) {
    "{ self.^name }.new({
        self.Hash.pairs.sort.map(-> (:$key, :$value) {
            next if $key eq <inflate deflate>.one;
            "$key.Str() => $value.perl()"
        }).join: ", "
    })"
}

method Hash(--> Hash()) {
    %(
        |(:attr($_)                 with $!attr             ),
        |(:attr-name($_)            with $!attr-name        ),
        |(:id($_)                   with $!id               ),
        |(:auto-increment($_)       with $!auto-increment   ),
        |(:references($_)           with &!references       ),
        |(:actual-references($_)    with &!actual-references),
        |(:ref($_)                  with $!ref              ),
        |(:nullable($_)             with $!nullable         ),
        |(:name($_)                 with $!name             ),
        |(:name-alias($_)           with $!name-alias       ),
        |(:type($_)                 with $!type             ),
        |(:inflate($_)              with &!inflate          ),
        |(:deflate($_)              with &!deflate          ),
        |(:computation($_)          with $!computation      ),
        |(:model-name($_)           with $!model-name       ),
        |(:column-name($_)          with $!column-name      ),
        |(:require($_)              with $!require          ),
    )
}

#| Returns a Hash that represents the column for migration purposes
method migration-hash(--> Hash()) {
    |(:name($_)                                 with $!name             ),
    |(:type(.type.^name)                        with $!attr             ),
    |(:references-table(.attr.package.^table)   with $.ref              ),
    |(:references-column(.column-name)          with $.ref              ),
    |(:is-id($_)                                with $!id               ),
    |(:is-auto-increment($_)                    with $!auto-increment   ),
    |(:is-nullable($_)                          with $!nullable         ),
}

#| Subclass used to lazy evaluation of parameter types
class ReferencesProxy does Callable {
    has Str     $.model-name    is required;
    has Str     $.column-name;
    has Str     $.require       = $!model-name;
    has         $.model;
    has         &.references;
    has Bool    $!tried-model   = False;

    method model($alias = Nil --> Mu:U) {
        if !$!tried-model {
            my $model = ::($!model-name);
            if !$model && $model ~~ Failure {
                require ::($!require);
                $model = ::($!model-name);
            }
            $!model = $model;
            $!tried-model = True;
        }
        do if $alias !=== Nil {
            do if $alias.^table eq $!model.^table {
                $alias
            } else {
                die "$alias.^name() isn't an alias for the table $!model.^table()"
            }
        } else {
            $!model
        }
    }

    method CALL-ME($alias = Nil) {
        my $*RED-INTERNAL = True;
        if &!references {
            my $model = self.model($alias);
            my $ret = &!references.($model);
            if $ret ~~ Red::Column && $ret.class.^name eq '$?CLASS' {
                $ret .= clone: :class($model)
            }
            $ret
        }
        else {
            self.model($alias).^columns.first(*.column.attr-name eq $!column-name).column
        }
    }
}

#| Returns the class that column is part of.
#method class { self.attr.package }

#| Method that returns the comment for the column
method comment { .Str with self.attr.WHY }

#| Returns a function that will return a column that is referenced by this column
method references(--> Callable) is rw {
    &!actual-references //= do {
        if &!references {
            if $!model-name {
                ReferencesProxy.new(:&!references, :$!model-name, :$!require, :$!model-type);
            }
            else {
                &!references;
            }
        }
        elsif $!model-name && $!column-name {
            ReferencesProxy.new(:$!model-name, :$!column-name, :$!require, :$!model-type);
        }
        else {
            Callable
        }
    }
}

#| Returns the column that is referenced by this one.
method ref($model = $!model-type !=== Red::Model ?? $!model-type !! Nil) {
    my $*RED-INTERNAL = True;
    .($model) with self.references
}

#| Required by the Red::AST role
method returns { $!attr.type }

method transpose(&func) { func self }

method gist { "{$!attr.package.HOW.^can("as") ?? $!attr.package.^as !! "({ $!attr.package.^name })"}.{$!name-alias}" }

method cast(Str $type) {
    Red::AST::Cast.new: self, $type
}

method find-column-name {
    $!attr-name
}

#| Returns an alias of that column
method alias(Str $name) {
    self.clone: name-alias => $name
}

#| Returns a clone using a different name
method as(Str $name, :$nullable = True) {
    self.clone: attr-name => $name, :$name, id => False, :$nullable, attr => Attribute
}

submethod TWEAK(:$unique) {
    if $!attr.type.^find_method: "red-type-db-methods" {
        self does $!attr.type.red-type-db-methods
    }
    with $unique {
        when Bool {
            $!attr.package.^add-unique-constraint: { self }
        }
        when Positional {
            self.unique-groups.append: |$unique
        }
        default {
            self.unique-groups.push: $unique
        }
    }
}

#| Do not test definedness, but returns a new Red::AST::IsDefined.
#| It's used to test `IS NULL` on the given column. It's also used
#| by any construction that naturally uses `.defined`.

method defined {
    Red::AST::IsDefined.new: self
}

method args {}

method not { Red::AST::Not.new: self }


