use nqp;
use Red::Attr::Column;
use Red::Attr::Relationship;

=head2 MetamodelX::Red::Dirtable

unit role MetamodelX::Red::Dirtable;

has %.dirty-cols{Mu} is rw;
has $!col-data-attr;

method col-data-attr(|) {
    $!col-data-attr;
}

sub col-data-attr-build(|){
    {}
}

has $!dirty-cols-attr;

method dirty-cols-attr(|) {
    $!dirty-cols-attr;
}

sub dirty-cols-attr-build(|) {
    SetHash.new
}

has $!dirty-old-values-attr;

method dirty-old-values-attr(|) {
    $!dirty-old-values-attr;
}

sub dirty-old-values-attr-build(|) {
    {}
}

method set-helper-attrs(Mu \type) {
    my %attr = type.^attributes.map( -> $a { $a.name => $a }).Hash;
    if %attr<%!___COLUMNS_DATA___>  -> $a {
        $!col-data-attr //= $a;
    }
    else {
        $!col-data-attr = Attribute.new: :name<%!___COLUMNS_DATA___>, :package(type), :type(Any), :!has_accessor;
        $!col-data-attr.set_build: &col-data-attr-build;
        type.^add_attribute: $!col-data-attr;
    }
    if %attr<%!___DIRTY_COLS_DATA___> -> $a {
        $!dirty-cols-attr //= $a;
    }
    else {
        $!dirty-cols-attr = Attribute.new: :name<%!___DIRTY_COLS_DATA___>, :package(type), :type(Any), :!has_accessor;
        $!dirty-cols-attr.set_build: &dirty-cols-attr-build;
        type.^add_attribute: $!dirty-cols-attr;
    }
    if %attr<%!___DIRTY_OLD_DATA___> -> $a {
        $!dirty-old-values-attr //= $a;
    }
    else {
        $!dirty-old-values-attr = Attribute.new: :name<%!___DIRTY_OLD_DATA___>, :package(type), :type(Any), :!has_accessor;
        $!dirty-old-values-attr.set_build: &dirty-old-values-attr-build;
        type.^add_attribute: $!dirty-old-values-attr;
    }
}

submethod !TWEAK_pr(\instance: *%data) is rw {
    my @columns = instance.^columns;

    my %new = |@columns.map: {
        my Mu $built := .build;
        $built := $built.(self.WHAT, Mu) if $built ~~ Method;
        next if $built =:= Mu;
        if instance.^is-id: $_ {
            instance.^set-id: .name => $built
        }
        .column.attr-name => $built
    };

    for %data.kv -> $k, $v { %new{$k} = $v }

    my $col-data-attr         := self.^col-data-attr;
    my $dirty-old-values-attr := self.^dirty-old-values-attr;
    $col-data-attr.set_value: instance, %new;
    for @columns -> \col {
        my \proxy = Proxy.new:
            FETCH => method {
                $col-data-attr.get_value(instance).{ col.column.attr-name }
            },
            STORE => method (\value) is hidden-from-backtrace {
                die X::Assignment::RO.new(value => $col-data-attr.get_value(instance).{ col.column.attr-name }) unless col.rw;
                die X::TypeCheck::Assignment.new(symbol => col.name, got => value, expected => col.type) unless value ~~ col.type;
                if instance.^is-id: col {
                    instance.^set-id: col.name => value
                }
                instance.^set-dirty: col;
                $dirty-old-values-attr.get_value(instance).{ col.column.attr-name } =
                    $col-data-attr.get_value(instance).{ col.column.attr-name };
                $col-data-attr.get_value(instance).{ col.column.attr-name } = value
            }
        #use nqp;
        #nqp::bindattr(nqp::decont(instance), self.WHAT, col.name, proxy);
        col.set_value: instance<>, proxy
    }
    for self.^attributes -> $attr {
        with %data{ $attr.name.substr: 2 } {
            unless $attr ~~ Red::Attr::Column {
                if self.^is-id: $attr {
                    self.^set-id: $attr.name => $_
                }
                $attr.set_value: self, $_
            }
        }
        # TODO: this should be on M::R::Relationship
        if $attr ~~ Red::Attr::Relationship {
            with %data{ $attr.name.substr: 2 } {
                $attr.set-data: instance, $_
            } else {
                my Mu $built := $attr.build;
                $built := $built.(self.WHAT, Mu) if $built ~~ Method;
                $attr.set-data: instance, $_ with $built
            }
        }
    }

    nextsame
}

method compose-dirtable(Mu \type) {
    my \meta = self;
    #state &build //= self.^find_private_method("TWEAK_pr");
    my &build = method (\instance: *%data) is rw {
        my $*RED-INTERNAL = True;
        my @columns = instance.^columns;

        my %new = |@columns.map: {
            my Mu $built := .build;
            $built := $built.(self.WHAT, Mu) if $built ~~ Method;
            next if $built =:= Mu;
            if instance.^is-id: $_ {
                instance.^set-id: .name => $built
            }
            .column.attr-name => $built
        };

        for %data.kv -> $k, $v { %new{$k} = $v }


        my $col-data-attr         := self.^col-data-attr;
        for $col-data-attr.get_value(instance).kv -> $k, $v { %new{$k} = $v }

        my $dirty-old-values-attr := self.^dirty-old-values-attr;
        $col-data-attr.set_value: instance, %new;
        for @columns -> \col {
            my \proxy = Proxy.new:
                FETCH => method {
                    $col-data-attr.get_value(instance).{ col.column.attr-name }
                },
                STORE => method (\value) is hidden-from-backtrace {
                    die X::Assignment::RO.new(value => $col-data-attr.get_value(instance).{ col.column.attr-name }) unless col.rw;
                    die X::TypeCheck::Assignment.new(symbol => col.name, got => value, expected => col.type) unless value ~~ col.type;
                    if instance.^is-id: col {
                        instance.^set-id: col.name => value
                    }
                    instance.^set-dirty: col;
                    $dirty-old-values-attr.get_value(instance).{ col.column.attr-name } =
                        $col-data-attr.get_value(instance).{ col.column.attr-name };
                    $col-data-attr.get_value(instance).{ col.column.attr-name } = value
                }
            #use nqp;
            #nqp::bindattr(nqp::decont(instance), self.WHAT, col.name, proxy);
            col.set_value: instance<>, proxy
        }
        for self.^attributes -> $attr {
            with %data{ $attr.name.substr: 2 } {
                unless $attr ~~ Red::Attr::Column {
                    if self.^is-id: $attr {
                        self.^set-id: $attr.name => $_
                    }
                    $attr.set_value: self, $_
                }
            }
            # TODO: this should be on M::R::Relationship
            if $attr ~~ Red::Attr::Relationship {
                with %data{ $attr.name.substr: 2 } {
                    $attr.set-data: instance, $_
                } else {
                    my Mu $built := $attr.build;
                    $built := $built.(self.WHAT, Mu) if $built ~~ Method;
                    $attr.set-data: instance, $_ with $built
                }
            }
        }

        nextsame
    }

    my role DirtableWrapped { };

    if self.declares_method(type, "TWEAK") {
        my &tweak = self.find_method(type, "TWEAK", :no_fallback(1));
        if &tweak !~~ DirtableWrapped {
            &tweak.wrap: &build;
            &tweak does DirtableWrapped;
        }
    } else {
        self.add_method: type, "TWEAK", &build;
    }
}

#| Accepts a Set of attributes of model and enables dirtiness flag for them,
#| which means that the values were changed and need a database sync.
multi method set-dirty(\obj, Set() $attr) {
    $!dirty-cols-attr.get_value(obj).{$_}++ for $attr.keys
}

multi method clean-up-columns(\obj, Set() $attr) {
    $!dirty-cols-attr.get_value(obj).{$_}-- for $attr.keys
}

#| Returns `True` if any of the object attributes were changed
#| from original database record values.
method is-dirty(Any:D \obj --> Bool) { so $!dirty-cols-attr.get_value(obj) }
#| Returns dirty columns of the object.
method dirty-columns(Any:D \obj)     { $!dirty-cols-attr.get_value(obj) }
#| Erases dirty status from all model's attributes, but does not (!)
#| revert their values to original ones.
method clean-up(Any:D \obj) {
    $!dirty-cols-attr.set_value: obj, SetHash.new;
    $!dirty-old-values-attr.set_value: obj, {}
}

multi method get-attr(\instance, Str $name) {
    $!col-data-attr.get_value(instance).{ $name }
}

multi method set-attr(\instance, Str $name, \value) {
    $!col-data-attr.get_value(instance).{ $name } = value
}

multi method get-attr(\instance, Red::Attr::Column $attr) {
    samewith instance, $attr.column.attr-name
}

multi method set-attr(\instance, Red::Attr::Column $attr, \value) {
    samewith instance, $attr.column.attr-name, value
}

multi method get-old-attr(\instance, Str $name) {
    $!dirty-old-values-attr.get_value(instance).{ $name }
}

multi method set-old-attr(\instance, Str $name, \value) {
    $!dirty-old-values-attr.get_value(instance).{ $name } = value
}

multi method get-old-attr(\instance, Red::Attr::Column $attr) {
    samewith instance, $attr.column.attr-name
}

multi method set-old-attr(\instance, Red::Attr::Column $attr, \value) {
    samewith instance, $attr.column.attr-name, value
}