### ----------------------------------------------------
### -- Chess
### -- Licenses: Artistic-2.0
### -- Authors: Lucien Grondin
### -- File: lib/Chess/Position.rakumod
### ----------------------------------------------------
### -- Chunk 1 of 3
use Chess::Board;
unit class Chess::Position is Chess::Board;
# translated from https://github.com/jhlywa/chess.js.git,
# albeit heavily modified by now
=begin original-licence
Copyright (c) 2025, Jeff Hlywa (jhlywa@gmail.com)
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
=end original-licence

use Chess::Colors;
use Chess::Pieces;
use Chess::Moves;

subset Check     of ::?CLASS is export where *.isCheck;
subset Checkmate of    Check is export where { .moves == 0 };

constant startpos = q{rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1};

enum castling-right <kingside queenside>;

has color $.turn;
has Set[castling-right] %.castling-rights{color};
has en-passant-square $.en-passant is rw;
has UInt ($.half-moves-count, $.move-number);
has Move $.last-move;

method reset-half-moves-count { $!half-moves-count = 0 }

sub infix:</>(Move $move, ::?CLASS $position) returns Move is export {
    use Chess::Board;
    my Str $lan = $move.LAN;
    given $position{$move.from} {
	when king {
	    if file($move.from) == file(e1) {
		given file($move.to) {
		    when file(g1) { return  KingsideCastle.new: :color($position.turn) }
		    when file(c1) { return QueensideCastle.new: :color($position.turn) }
		}
	    }
	}
	when pawn {
	    my PawnMove $pawn-move .= new: $lan;
	    with $position.en-passant {
		$pawn-move does EnPassant if $pawn-move.to == $_;
	    }
	    return $pawn-move;
	}
    }
    return $move;
}

method attackers(Square :$square, color :$color = $!turn) {
    self.Chess::Board::attackers(:$square, :$color).Set
}

multi method new(Str $fen where { Chess::FEN.parse: $fen } = startpos) {
    use Chess::FEN;
    my color $turn;
    my Set[castling-right] %castling-rights{color};
    my en-passant-square $en-passant;
    my UInt ($half-moves-count, $move-number);
    Chess::FEN.parse:
    $fen,
    actions => class {
	method active-color($/)     { $turn = $/ eq 'w' ?? white !! black }
	method half-move-clock($/)  { $half-moves-count = +$/ }
	method en-passant($/)       { $en-passant = square-enum::{~$/} unless $/ eq '-' }
	method castling($/)         {
	    return if $/ eq '-';
	    for (white) => rx/<[KQ]>/, (black) => rx/<[kq]>/ {
		%castling-rights{.key} = Set[castling-right].new:
		%(<k q> Z=> kingside, queenside){$/.Str.comb(.value)».lc};
	    }
	}
	method full-move-number($/) { $move-number = +$/ }
    }.new;
    self.bless: :board($fen.words.head), :$turn, :%castling-rights, :$en-passant, :$half-moves-count, :$move-number;
}

multi method new(::?CLASS:D: Move::FullyDefined $move) {
    my $new = self.bless:
	board            => self.fen.words.head,
	turn             => ¬self.turn,
	castling-rights  => Hash[Set[castling-right],color].new(self.castling-rights<>),
	half-moves-count => self.half-moves-count + 1,
	move-number      => self.turn ~~ black ?? self.move-number + 1 !! self.move-number
	;
    with $new{$move.from} {
	my $color = .&Chess::Pieces::get-color;
	when king {
	    for kingside, queenside -> $right { $new.deprive-of-castling-right($right, :$color) }
	}
	when rook {
	    if kingside  ∈ $new.castling-rights{$color} and $move.from == ($color ~~ white ?? h1 !! h8) {
		$new.deprive-of-castling-right(kingside, :$color);
	    }
	    if queenside ∈ $new.castling-rights{$color} and $move.from == ($color ~~ white ?? a1 !! a8) {
		$new.deprive-of-castling-right(queenside, :$color);
	    }
	}
	when pawn {
	    $new.reset-half-moves-count;
	    $new.en-passant = $move.to - get-offsets($_)[0] if $move ~~ BigPawnMove;
	}
    }
    else { fail "there is no piece on square {square-enum($move.from)}:\n{self.ascii}"; }
    with $new{$move.to} {
	if self{$move.from} ≡ .&Chess::Pieces::get-color {
	    fail "can't capture a piece of the same color (move is {$move.raku}):\n{self.ascii}";
	}
	$new.reset-half-moves-count;
    }
    $move.move-pieces($new);
    $new;
}

method deprive-of-castling-right(castling-right $castling-right, color :$color) {
    my Set[castling-right] $castling-rights = %!castling-rights{$color}<>;
    %!castling-rights{$color} (-)= $castling-right;
    return -> { %!castling-rights{$color} = $castling-rights<> }
}

multi method make(::?CLASS:D: Str $move-string) {
    samewith Move.new: $move-string, :color(self.turn), :board(self)
}
multi method make(::?CLASS:D: Move::FullyDefined $move) {
    my @undo;
    $!move-number++ if $!turn ~~ black;        @undo.push: -> { $!move-number-- if $!turn ~~ black }
    $!turn = ¬$!turn;                          @undo.push: -> { $!turn = ¬$!turn }
    $!half-moves-count++;                      @undo.push: -> { $!half-moves-count-- }
    my $en-passant = $!en-passant;
    $!en-passant = Nil;                        @undo.push: -> { $!en-passant = $en-passant }
    with self{$move.from} {
	my $color = .&Chess::Pieces::get-color;
	when king {
	    for kingside, queenside -> $right {
		if $right ∈ %!castling-rights{$color} {
		    @undo.push: self.deprive-of-castling-right($right, :$color)
		}
	    }
	}
	when rook {
	    if kingside  ∈ self.castling-rights{$color} and $move.from == ($color ~~ white ?? h1 !! h8) {
		@undo.push: self.deprive-of-castling-right(kingside, :$color);
	    }
	    if queenside ∈ self.castling-rights{$color} and $move.from == ($color ~~ white ?? a1 !! a8) {
		@undo.push: self.deprive-of-castling-right(queenside, :$color);
	    }
	}
	when pawn {
	    my $half-moves-count = $!half-moves-count;
	    $!half-moves-count = 0; @undo.push: -> { $!half-moves-count = $half-moves-count }
	    if abs(rank($move.from) - rank($move.to)) == 2 {
		my $en-passant = $!en-passant;
		$!en-passant = $move.to - get-offsets($_)[0];
		@undo.push: -> { $!en-passant = $en-passant }
	    }
	}
    }
    else { fail "there is no piece on square {square-enum($move.from)}:\n{self.ascii}"; }
    with self{$move.to} {
	my $color = .&Chess::Pieces::get-color;
	when rook {
	    if kingside  ∈ self.castling-rights{$color} and $move.to == ($color ~~ white ?? h1 !! h8) {
		@undo.push: self.deprive-of-castling-right(kingside, :$color);
	    }
	    if queenside ∈ self.castling-rights{$color} and $move.to == ($color ~~ white ?? a1 !! a8) {
		@undo.push: self.deprive-of-castling-right(queenside, :$color);
	    }
	    proceed;
	}
	if self{$move.from} ≡ Chess::Pieces::get-color($_) {
	    fail "can't capture a piece of the same color (move is {$move.raku}):\n{self.ascii}";
	}
	my $half-moves-count = $!half-moves-count;
	$!half-moves-count = 0;
	@undo.push: -> { $!half-moves-count = $half-moves-count }
    }
    @undo.push: $move.move-pieces(self);
    for black, white -> $color {
	fail "too many $color kings after move {$move.LAN}"
	if self{$color ~~ white ?? wk !! bk}.elems > 1;
    }
    return -> {
	@undo.pop.() while @undo;
    }
}

method gist { self.fen }
method fen returns Str {
    join ' ',
    do for 8,7...1 -> $r {
	do for 'a'..'h' -> $c {
	    my $piece = self{square-enum::{"$c$r"}};
	    $piece ?? symbol($piece) !! '1'
	}.join
	.subst(/1+/, { .chars }, :g)
    }.join("/"),
    $!turn eq white ?? 'w' !! 'b',
    (
	%!castling-rights{white} ?? %(kingside, queenside Z=> <K Q>){%!castling-rights{white}.keys} !! '',
	%!castling-rights{black} ?? %(kingside, queenside Z=> <k q>){%!castling-rights{black}.keys} !! ''
    ).flat.sort.join.subst(/^$/, '-'),
    $!en-passant.defined ?? square-enum($!en-passant) !! '-',
    $!half-moves-count,
    $!move-number
}

method moves(Bool :$legal = True, piece :$piece, UInt :$square) {
    my ($us, $them) = $!turn, ¬$!turn;
    my @squares = $square.defined ?? ($square,) !! @Chess::Board::squares;

    my @ = .<>.cache given (state %cache){self.fen}{$legal}{$piece.defined ?? symbol($piece) !! 'all'}{$square // 'all'} //=
    gather {
	for @squares -> Square $from {
	    next if self{$from}:!exists || self{$from} ≡ $them;
	    if self{$from} ~~ pawn {
		my pawn $pawn = self{$from};
		next if $piece.defined && $piece !~~ pawn;
		my $to = $from + get-offsets($pawn)[0];
		if !self{$to} {
		    if rank($to) == $PROMOTION-RANK {
			for wq, wn, wr, wb -> $promotion {
			    take PawnMove.new(:$from, :$to) but Promotion[self.turn ~~ white ?? $promotion !! ¬$promotion];
			}
		    } else { take PawnMove.new: :$from, :$to; }
		    $to = $from + get-offsets($pawn)[1];
		    if $to ~~ Square {
			if rank($from) == %SECOND-RANK{$us} && !self{$to} {
			    take PawnMove.new(:$from, :$to);
			}
		    }
		}
		for 2..^4 -> $j {
		    $to = $from + get-offsets($pawn)[$j];
		    next unless $to ~~ Square;
		    if self{$to}.defined && self{$to} ≡ $them {
			if rank($to) == $PROMOTION-RANK {
			    for wq, wn, wr, wb -> $promotion {
				my PawnMove $pawn-move .= new: :$from, :$to;
				$pawn-move does Promotion[self.turn ~~ white ?? $promotion !! ¬$promotion];
				$pawn-move does capture;
				take $pawn-move;
			    }
			} else { take PawnMove.new(:$from, :$to) but capture; }
		    } elsif $!en-passant && $to ~~ $!en-passant {
			take PawnMove.new(:$from, :$to) but EnPassant;
		    }
		}
	    }
	    else {
		next if $piece.defined && self{$from} !~~ $piece;
		for get-offsets(self{$from}) -> $offset {
		    my $to = $from;
		    loop {
			$to += $offset;
			last unless $to ~~ Square;
			with self{$to} {
			    last if $_ ≡ $us;
			    take Move.new(:$from, :$to) but capture;
			    last;
			} else {
			    take Move.new: :$from, :$to;
			}
			last if self{$from} ~~ king|knight;
		    }
		}
	    }
	}
	if !$piece.defined || $piece ~~ king {
	    my $our-king-location = self{$us ~~ white ?? wk !! bk}.pick;
	    if $piece ~~ king || $square.defined && $square == $our-king-location {
		if kingside ∈ %!castling-rights{$us} {
		    my Square $castling-from = $our-king-location;
		    my Square $castling-to   = $castling-from + 2;
		    if 
			!self{$castling-from + 1} &&
			!self{$castling-to} &&
			!self.attacked(:color($them), :square($our-king-location)) &&
			!self.attacked(:color($them), :square($castling-from + 1)) &&
			!self.attacked(:color($them), :square($castling-to))
		    {
			take KingsideCastle.new: :from($our-king-location), :to($castling-to)
		    }
		}
		if queenside ∈ %!castling-rights{$us} {
		    my Square $castling-from = $our-king-location;
		    my Square $castling-to   = $castling-from - 2;
		    if 
			!self{Square($castling-from - 1)} &&
			!self{$castling-to} &&
			!self.attacked(:color($them), :square($our-king-location)) &&
			!self.attacked(:color($them), :square($castling-from - 1)) &&
			!self.attacked(:color($them), :square($castling-to))
		    {
			take QueensideCastle.new: :from($our-king-location), :to($castling-to)
		    }
		}
	    }
	}
    }.grep:
	!$legal ?? * !! sub ($_) {
	    my $fen = self.fen;
	    my $wk-deal = self{.from} ~~ wk;
	    my &undo = self.make($_);
	    LEAVE {
		&undo();
		fail "could not undo move properly" unless self.fen eq $fen;
	    }
	    return !self.isKingAttacked($us);
	}
}

method isCheck     returns Bool {  self.isKingAttacked($!turn) }
method inCheck     returns Bool {  self.isCheck }
method isCheckmate returns Bool {  self.isCheck && self.moves.elems == 0 }
method isStalemate returns Bool { !self.isCheck && self.moves.elems == 0 }

method isInsufficientMaterial returns Bool {
    my UInt %pieces{piece} = (pawn, bishop, knight, rook, queen, king) X=> 0;
    my @bishops;
    my UInt ($numPieces, $squareColor) = 0, 0;
    for @Chess::Board::squares -> $square {
	$squareColor = (&rank, &file)».($square).sum % 2;
	if self{$square}:exists {
	    my $piece = self{$square};
	    %pieces{$piece.WHAT}++;
	    if $piece ~~ bishop {
		@bishops.push: $squareColor;
	    }
	    $numPieces++
	}
    }
    if $numPieces == 2 {
	return True;
    } elsif $numPieces == 3 && (%pieces{bishop} == 1 || %pieces{knight} == 1) {
	return True;
    } elsif $numPieces == %pieces{bishop} + 2 {
	my UInt $sum = 0;
	$sum += $_ for @bishops;
	if $sum == 0|@bishops.elems {
	    return True
	}
    }
    return False;
}

method WHICH { self.uint.base(36) }
# http://hgm.nubati.net/book_format.html
method uint returns uint64 {
    constant $Random64 = Blob[uint64].new: map *.parse-base(16), <
	9D39247E33776D41 2AF7398005AAA5C7 44DB015024623547 9C15F73E62A76AE2
	75834465489C0C89 3290AC3A203001BF 0FBBAD1F61042279 E83A908FF2FB60CA
	0D7E765D58755C10 1A083822CEAFE02D 9605D5F0E25EC3B0 D021FF5CD13A2ED5
	40BDF15D4A672E32 011355146FD56395 5DB4832046F3D9E5 239F8B2D7FF719CC
	05D1A1AE85B49AA1 679F848F6E8FC971 7449BBFF801FED0B 7D11CDB1C3B7ADF0
	82C7709E781EB7CC F3218F1C9510786C 331478F3AF51BBE6 4BB38DE5E7219443
	AA649C6EBCFD50FC 8DBD98A352AFD40B 87D2074B81D79217 19F3C751D3E92AE1
	B4AB30F062B19ABF 7B0500AC42047AC4 C9452CA81A09D85D 24AA6C514DA27500
	4C9F34427501B447 14A68FD73C910841 A71B9B83461CBD93 03488B95B0F1850F
	637B2B34FF93C040 09D1BC9A3DD90A94 3575668334A1DD3B 735E2B97A4C45A23
	18727070F1BD400B 1FCBACD259BF02E7 D310A7C2CE9B6555 BF983FE0FE5D8244
	9F74D14F7454A824 51EBDC4AB9BA3035 5C82C505DB9AB0FA FCF7FE8A3430B241
	3253A729B9BA3DDE 8C74C368081B3075 B9BC6C87167C33E7 7EF48F2B83024E20
	11D505D4C351BD7F 6568FCA92C76A243 4DE0B0F40F32A7B8 96D693460CC37E5D
	42E240CB63689F2F 6D2BDCDAE2919661 42880B0236E4D951 5F0F4A5898171BB6
	39F890F579F92F88 93C5B5F47356388B 63DC359D8D231B78 EC16CA8AEA98AD76
	5355F900C2A82DC7 07FB9F855A997142 5093417AA8A7ED5E 7BCBC38DA25A7F3C
	19FC8A768CF4B6D4 637A7780DECFC0D9 8249A47AEE0E41F7 79AD695501E7D1E8
	14ACBAF4777D5776 F145B6BECCDEA195 DABF2AC8201752FC 24C3C94DF9C8D3F6
	BB6E2924F03912EA 0CE26C0B95C980D9 A49CD132BFBF7CC4 E99D662AF4243939
	27E6AD7891165C3F 8535F040B9744FF1 54B3F4FA5F40D873 72B12C32127FED2B
	EE954D3C7B411F47 9A85AC909A24EAA1 70AC4CD9F04F21F5 F9B89D3E99A075C2
	87B3E2B2B5C907B1 A366E5B8C54F48B8 AE4A9346CC3F7CF2 1920C04D47267BBD
	87BF02C6B49E2AE9 092237AC237F3859 FF07F64EF8ED14D0 8DE8DCA9F03CC54E
	9C1633264DB49C89 B3F22C3D0B0B38ED 390E5FB44D01144B 5BFEA5B4712768E9
	1E1032911FA78984 9A74ACB964E78CB3 4F80F7A035DAFB04 6304D09A0B3738C4
	2171E64683023A08 5B9B63EB9CEFF80C 506AACF489889342 1881AFC9A3A701D6
	6503080440750644 DFD395339CDBF4A7 EF927DBCF00C20F2 7B32F7D1E03680EC
	B9FD7620E7316243 05A7E8A57DB91B77 B5889C6E15630A75 4A750A09CE9573F7
	CF464CEC899A2F8A F538639CE705B824 3C79A0FF5580EF7F EDE6C87F8477609D
	799E81F05BC93F31 86536B8CF3428A8C 97D7374C60087B73 A246637CFF328532
	043FCAE60CC0EBA0 920E449535DD359E 70EB093B15B290CC 73A1921916591CBD
	56436C9FE1A1AA8D EFAC4B70633B8F81 BB215798D45DF7AF 45F20042F24F1768
	930F80F4E8EB7462 FF6712FFCFD75EA1 AE623FD67468AA70 DD2C5BC84BC8D8FC
	7EED120D54CF2DD9 22FE545401165F1C C91800E98FB99929 808BD68E6AC10365
	DEC468145B7605F6 1BEDE3A3AEF53302 43539603D6C55602 AA969B5C691CCB7A
	A87832D392EFEE56 65942C7B3C7E11AE DED2D633CAD004F6 21F08570F420E565
	B415938D7DA94E3C 91B859E59ECB6350 10CFF333E0ED804A 28AED140BE0BB7DD
	C5CC1D89724FA456 5648F680F11A2741 2D255069F0B7DAB3 9BC5A38EF729ABD4
	EF2F054308F6A2BC AF2042F5CC5C2858 480412BAB7F5BE2A AEF3AF4A563DFE43
	19AFE59AE451497F 52593803DFF1E840 F4F076E65F2CE6F0 11379625747D5AF3
	BCE5D2248682C115 9DA4243DE836994F 066F70B33FE09017 4DC4DE189B671A1C
	51039AB7712457C3 C07A3F80C31FB4B4 B46EE9C5E64A6E7C B3819A42ABE61C87
	21A007933A522A20 2DF16F761598AA4F 763C4A1371B368FD F793C46702E086A0
	D7288E012AEB8D31 DE336A2A4BC1C44B 0BF692B38D079F23 2C604A7A177326B3
	4850E73E03EB6064 CFC447F1E53C8E1B B05CA3F564268D99 9AE182C8BC9474E8
	A4FC4BD4FC5558CA E755178D58FC4E76 69B97DB1A4C03DFE F9B5B7C4ACC67C96
	FC6A82D64B8655FB 9C684CB6C4D24417 8EC97D2917456ED0 6703DF9D2924E97E
	C547F57E42A7444E 78E37644E7CAD29E FE9A44E9362F05FA 08BD35CC38336615
	9315E5EB3A129ACE 94061B871E04DF75 DF1D9F9D784BA010 3BBA57B68871B59D
	D2B7ADEEDED1F73F F7A255D83BC373F8 D7F4F2448C0CEB81 D95BE88CD210FFA7
	336F52F8FF4728E7 A74049DAC312AC71 A2F61BB6E437FDB5 4F2A5CB07F6A35B3
	87D380BDA5BF7859 16B9F7E06C453A21 7BA2484C8A0FD54E F3A678CAD9A2E38C
	39B0BF7DDE437BA2 FCAF55C1BF8A4424 18FCF680573FA594 4C0563B89F495AC3
	40E087931A00930D 8CFFA9412EB642C1 68CA39053261169F 7A1EE967D27579E2
	9D1D60E5076F5B6F 3810E399B6F65BA2 32095B6D4AB5F9B1 35CAB62109DD038A
	A90B24499FCFAFB1 77A225A07CC2C6BD 513E5E634C70E331 4361C0CA3F692F12
	D941ACA44B20A45B 528F7C8602C5807B 52AB92BEB9613989 9D1DFA2EFC557F73
	722FF175F572C348 1D1260A51107FE97 7A249A57EC0C9BA2 04208FE9E8F7F2D6
	5A110C6058B920A0 0CD9A497658A5698 56FD23C8F9715A4C 284C847B9D887AAE
	04FEABFBBDB619CB 742E1E651C60BA83 9A9632E65904AD3C 881B82A13B51B9E2
	506E6744CD974924 B0183DB56FFC6A79 0ED9B915C66ED37E 5E11E86D5873D484
	F678647E3519AC6E 1B85D488D0F20CC5 DAB9FE6525D89021 0D151D86ADB73615
	A865A54EDCC0F019 93C42566AEF98FFB 99E7AFEABE000731 48CBFF086DDF285A
	7F9B6AF1EBF78BAF 58627E1A149BBA21 2CD16E2ABD791E33 D363EFF5F0977996
	0CE2A38C344A6EED 1A804AADB9CFA741 907F30421D78C5DE 501F65EDB3034D07
	376