<?php

namespace ccxt\pro;

function bisectLeft($arr, $x) {
    $low = 0;
    $high = count($arr) - 1;
    while ($low <= $high) {
        $mid = intdiv(($low + $high), 2);
        if ($arr[$mid] - $x < 0) $low = $mid + 1;
        else $high = $mid - 1;
    }
    return $low;
}

const tmp = array();

class OrderBookSide extends \ArrayObject implements \JsonSerializable {
    public $index;
    public $depth;
    public $n;

    public function __construct($deltas = array(), $depth = null) {
        parent::__construct();
        $this->depth = $depth ? $depth : PHP_INT_MAX;
        $this->n = PHP_INT_MAX;
        $this->index = array();

        foreach ($deltas as $delta) {
            $this->storeArray($delta);
        }
    }

    public function store_array($delta) {
        return $this->storeArray($delta);
    }

    public function storeArray($delta) {
        $price = $delta[0];
        $size = $delta[1];
        $index_price = static::$side ? -$price : $price;
        $index = bisectLeft($this->index, $index_price);
        if ($size) {
            if ($index < count($this->index) && $this->index[$index] === $index_price) {
                $tmp = $this->exchangeArray(tmp);
                $tmp[$index][1] = $size;
                $this->exchangeArray($tmp);
            } else {
                array_splice($this->index, $index, 0, $index_price);
                $tmp = $this->exchangeArray(tmp);
                array_splice($tmp, $index, 0, array($delta));
                $this->exchangeArray($tmp);
            }
        } elseif ($index < count($this->index) && $this->index[$index] === $index_price) {
            // delete from index and self
            array_splice($this->index, $index, 1);
            $tmp = $this->exchangeArray(tmp);
            array_splice($tmp, $index, 1);
            $this->exchangeArray($tmp);
        }
    }

    public function store($price, $size, $id = null) {
        $this->storeArray(array($price, $size));
    }

    public function limit() {
        $difference = count($this) - $this->depth;
        if ($difference > 0) {
            array_splice($this->index, -$difference);
            $tmp = $this->exchangeArray(tmp);
            array_splice($tmp, -$difference);
            $this->exchangeArray($tmp);
        }
    }

    public function JsonSerialize () : array {
        $copy = $this->getArrayCopy();
        if ($this->n > count($this)) {
            return $copy;
        } else {
            return array_slice($copy, 0, $this->n);
        }
    }

    #[\ReturnTypeWillChange]
    public function offsetExists($key) {
        return $key < $this->n && parent::offsetExists($key);
    }

    #[\ReturnTypeWillChange]
    public function offsetGet($key) {
        if ($key < $this->n) {
            return parent::offsetGet($key);
        }
    }

    #[\ReturnTypeWillChange]
    public function count() {
        return min($this->n, parent::count());
    }

    #[\ReturnTypeWillChange]
    public function __debugInfo() {

        $default = parent::__debugInfo(); // TODO: Change the autogenerated stub
        // unset($default['index']);
        return $default;
    }
}

// ----------------------------------------------------------------------------
// overwrites absolute volumes at price levels
// or deletes price levels based on order counts (3rd value in a bidask delta)

class CountedOrderBookSide extends OrderBookSide {
    public function __construct($deltas = array(), $depth = null) {
        parent::__construct($deltas, $depth);
    }

    public function store($price, $size, $id = null) {
        $this->storeArray(array($price, $size, $id));
    }

    public function storeArray($delta) {
        $price = $delta[0];
        $size = $delta[1];
        $count = $delta[2];
        $index_price = static::$side ? -$price : $price;
        $index = bisectLeft($this->index, $index_price);
        if ($size && $count) {
            if ($index < count($this->index) && $this->index[$index] === $index_price) {
                $tmp = $this->exchangeArray(tmp);
                $tmp[$index][1] = $size;
                $tmp[$index][2] = $count;
                $this->exchangeArray($tmp);
            } else {
                array_splice($this->index, $index, 0, $index_price);
                $tmp = $this->exchangeArray(tmp);
                array_splice($tmp, $index, 0, array($delta));
                $this->exchangeArray($tmp);
            }
        } elseif ($index < count($this->index) && $this->index[$index] === $index_price) {
            // delete from index and self
            array_splice($this->index, $index, 1);
            $tmp = $this->exchangeArray(tmp);
            array_splice($tmp, $index, 1);
            $this->exchangeArray($tmp);
        }
    }
}

// ----------------------------------------------------------------------------
// indexed by order ids (3rd value in a bidask delta)

class IndexedOrderBookSide extends OrderBookSide {
    public $hashmap;

    public function __construct($deltas = array(), $depth = PHP_INT_MAX) {
        $this->hashmap = array();
        parent::__construct($deltas, $depth);
    }

    public function store($price, $size, $id = null) {
        $this->storeArray(array($price, $size, $id));
    }

    public function storeArray($delta) {
        $price = $delta[0];
        $size = $delta[1];
        $id = $delta[2];
        $index_price = null;
        if ($price !== null) {
            $index_price = static::$side ? -$price : $price;
        }
        if ($size) {
            if (array_key_exists($id, $this->hashmap)) {
                $old_price = $this->hashmap[$id];
                $index_price = $index_price ? $index_price : $old_price;
                // in case price is not set
                $delta[0] = abs($index_price);
                if ($index_price === $old_price) {
                    $index = bisectLeft($this->index, $index_price);
                    while ($this[$index][2] != $id) {
                        $index++;
                    }
                    $this[$index] = $delta;
                    return;
                } else {
                    // remove old price from index
                    $old_index = bisectLeft($this->index, $old_price);
                    while ($this[$old_index][2] != $id) {
                        $old_index++;
                    }
                    array_splice($this->index, $old_index, 1);
                    $tmp = $this->exchangeArray(tmp);
                    array_splice($tmp, $old_index, 1);
                    $this->exchangeArray($tmp);
                }
            }
            // insert new price level into the orderbook
            $this->hashmap[$id] = $index_price;
            $index = bisectLeft($this->index, $index_price);
            while (array_key_exists($index, $this->index) && $this->index[$index] == $index_price && $this[$index][2] < $id) {
                $index++;
            }
            array_splice($this->index, $index, 0, $index_price);
            $tmp = $this->exchangeArray(tmp);
            array_splice($tmp, $index, 0, array($delta));
            $this->exchangeArray($tmp);
        } else if (array_key_exists($id, $this->hashmap)) {
            $old_price = $this->hashmap[$id];
            $index = bisectLeft($this->index, $old_price);
            while ($this[$index][2] != $id) {
                $index++;
            }
            array_splice($this->index, $index, 1);
            $tmp = $this->exchangeArray(tmp);
            array_splice($tmp, $index, 1);
            $this->exchangeArray($tmp);
            unset($this->hashmap[$id]);
        }
    }
}

// ----------------------------------------------------------------------------
// a more elegant syntax is possible here, but native inheritance is portable

class Asks extends OrderBookSide { public static $side = false; }
class Bids extends OrderBookSide { public static $side = true; }
class CountedAsks extends CountedOrderBookSide { public static $side = false; }
class CountedBids extends CountedOrderBookSide { public static $side = true; }
class IndexedAsks extends IndexedOrderBookSide { public static $side = false; }
class IndexedBids extends IndexedOrderBookSide { public static $side = true; }

// ----------------------------------------------------------------------------
