<?php

namespace dokuwiki\Parsing\Handler;

class Table extends AbstractRewriter
{
    protected $tableCalls = [];
    protected $maxCols = 0;
    protected $maxRows = 1;
    protected $currentCols = 0;
    protected $firstCell = false;
    protected $lastCellType = 'tablecell';
    protected $inTableHead = true;
    protected $currentRow = ['tableheader' => 0, 'tablecell' => 0];
    protected $countTableHeadRows = 0;

    /** @inheritdoc */
    public function finalise()
    {
        $last_call = end($this->calls);
        $this->writeCall(['table_end', [], $last_call[2]]);

        $this->process();
        $this->callWriter->finalise();
        unset($this->callWriter);
    }

    /** @inheritdoc */
    public function process()
    {
        foreach ($this->calls as $call) {
            switch ($call[0]) {
                case 'table_start':
                    $this->tableStart($call);
                    break;
                case 'table_row':
                    $this->tableRowClose($call);
                    $this->tableRowOpen(['tablerow_open', $call[1], $call[2]]);
                    break;
                case 'tableheader':
                case 'tablecell':
                    $this->tableCell($call);
                    break;
                case 'table_end':
                    $this->tableRowClose($call);
                    $this->tableEnd($call);
                    break;
                default:
                    $this->tableDefault($call);
                    break;
            }
        }
        $this->callWriter->writeCalls($this->tableCalls);

        return $this->callWriter;
    }

    protected function tableStart($call)
    {
        $this->tableCalls[] = ['table_open', $call[1], $call[2]];
        $this->tableCalls[] = ['tablerow_open', [], $call[2]];
        $this->firstCell = true;
    }

    protected function tableEnd($call)
    {
        $this->tableCalls[] = ['table_close', $call[1], $call[2]];
        $this->finalizeTable();
    }

    protected function tableRowOpen($call)
    {
        $this->tableCalls[] = $call;
        $this->currentCols = 0;
        $this->firstCell = true;
        $this->lastCellType = 'tablecell';
        $this->maxRows++;
        if ($this->inTableHead) {
            $this->currentRow = ['tablecell' => 0, 'tableheader' => 0];
        }
    }

    protected function tableRowClose($call)
    {
        if ($this->inTableHead && ($this->inTableHead = $this->isTableHeadRow())) {
            $this->countTableHeadRows++;
        }
        // Strip off final cell opening and anything after it
        while ($discard = array_pop($this->tableCalls)) {
            if ($discard[0] == 'tablecell_open' || $discard[0] == 'tableheader_open') {
                break;
            }
            if (!empty($this->currentRow[$discard[0]])) {
                $this->currentRow[$discard[0]]--;
            }
        }
        $this->tableCalls[] = ['tablerow_close', [], $call[2]];

        if ($this->currentCols > $this->maxCols) {
            $this->maxCols = $this->currentCols;
        }
    }

    protected function isTableHeadRow()
    {
        $td = $this->currentRow['tablecell'];
        $th = $this->currentRow['tableheader'];

        if (!$th || $td > 2) return false;
        if (2 * $td > $th) return false;

        return true;
    }

    protected function tableCell($call)
    {
        if ($this->inTableHead) {
            $this->currentRow[$call[0]]++;
        }
        if (!$this->firstCell) {
            // Increase the span
            $lastCall = end($this->tableCalls);

            // A cell call which follows an open cell means an empty cell so span
            if ($lastCall[0] == 'tablecell_open' || $lastCall[0] == 'tableheader_open') {
                $this->tableCalls[] = ['colspan', [], $call[2]];
            }

            $this->tableCalls[] = [$this->lastCellType . '_close', [], $call[2]];
            $this->tableCalls[] = [$call[0] . '_open', [1, null, 1], $call[2]];
            $this->lastCellType = $call[0];
        } else {
            $this->tableCalls[] = [$call[0] . '_open', [1, null, 1], $call[2]];
            $this->lastCellType = $call[0];
            $this->firstCell = false;
        }

        $this->currentCols++;
    }

    protected function tableDefault($call)
    {
        $this->tableCalls[] = $call;
    }

    protected function finalizeTable()
    {

        // Add the max cols and rows to the table opening
        if ($this->tableCalls[0][0] == 'table_open') {
            // Adjust to num cols not num col delimeters
            $this->tableCalls[0][1][] = $this->maxCols - 1;
            $this->tableCalls[0][1][] = $this->maxRows;
            $this->tableCalls[0][1][] = array_shift($this->tableCalls[0][1]);
        } else {
            trigger_error('First element in table call list is not table_open');
        }

        $lastRow = 0;
        $lastCell = 0;
        $cellKey = [];
        $toDelete = [];

        // if still in tableheader, then there can be no table header
        // as all rows can't be within <THEAD>
        if ($this->inTableHead) {
            $this->inTableHead = false;
            $this->countTableHeadRows = 0;
        }

        // Look for the colspan elements and increment the colspan on the
        // previous non-empty opening cell. Once done, delete all the cells
        // that contain colspans
        $key = -1;
        while (++$key < count($this->tableCalls)) {
            $call = $this->tableCalls[$key];

            switch ($call[0]) {
                case 'table_open':
                    if ($this->countTableHeadRows) {
                        array_splice($this->tableCalls, $key + 1, 0, [['tablethead_open', [], $call[2]]]);
                    }
                    break;

                case 'tablerow_open':
                    $lastRow++;
                    $lastCell = 0;
                    break;

                case 'tablecell_open':
                case 'tableheader_open':
                    $lastCell++;
                    $cellKey[$lastRow][$lastCell] = $key;
                    break;

                case 'table_align':
                    $prev = in_array($this->tableCalls[$key - 1][0], ['tablecell_open', 'tableheader_open']);
                    $next = in_array($this->tableCalls[$key + 1][0], ['tablecell_close', 'tableheader_close']);
                    // If the cell is empty, align left
                    if ($prev && $next) {
                        $this->tableCalls[$key - 1][1][1] = 'left';

                        // If the previous element was a cell open, align right
                    } elseif ($prev) {
                        $this->tableCalls[$key - 1][1][1] = 'right';

                        // If the next element is the close of an element, align either center or left
                    } elseif ($next) {
                        if ($this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] == 'right') {
                            $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'center';
                        } else {
                            $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'left';
                        }
                    }

                    // Now convert the whitespace back to cdata
                    $this->tableCalls[$key][0] = 'cdata';
                    break;

                case 'colspan':
                    $this->tableCalls[$key - 1][1][0] = false;

                    for ($i = $key - 2; $i >= $cellKey[$lastRow][1]; $i--) {
                        if (
                            $this->tableCalls[$i][0] == 'tablecell_open' ||
                            $this->tableCalls[$i][0] == 'tableheader_open'
                        ) {
                            if (false !== $this->tableCalls[$i][1][0]) {
                                $this->tableCalls[$i][1][0]++;
                                break;
                            }
                        }
                    }

                    $toDelete[] = $key - 1;
                    $toDelete[] = $key;
                    $toDelete[] = $key + 1;
                    break;

                case 'rowspan':
                    if ($this->tableCalls[$key - 1][0] == 'cdata') {
                        // ignore rowspan if previous call was cdata (text mixed with :::)
                        // we don't have to check next call as that wont match regex
                        $this->tableCalls[$key][0] = 'cdata';
                    } else {
                        $spanning_cell = null;

                        // can't cross thead/tbody boundary
                        if (!$this->countTableHeadRows || ($lastRow - 1 != $this->countTableHeadRows)) {
                            for ($i = $lastRow - 1; $i > 0; $i--) {
                                if (
                                    $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tablecell_open' ||
                                    $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tableheader_open'
                                ) {
                                    if ($this->tableCalls[$cellKey[$i][$lastCell]][1][2] >= $lastRow - $i) {
                                        $spanning_cell = $i;
                                        break;
                                    }
                                }
                            }
                        }
                        if (is_null($spanning_cell)) {
                            // No spanning cell found, so convert this cell to
                            // an empty one to avoid broken tables
                            $this->tableCalls[$key][0] = 'cdata';
                            $this->tableCalls[$key][1][0] = '';
                            break;
                        }
                        $this->tableCalls[$cellKey[$spanning_cell][$lastCell]][1][2]++;

                        $this->tableCalls[$key - 1][1][2] = false;

                        $toDelete[] = $key - 1;
                        $toDelete[] = $key;
                        $toDelete[] = $key + 1;
                    }
                    break;

                case 'tablerow_close':
                    // Fix broken tables by adding missing cells
                    $moreCalls = [];
                    while (++$lastCell < $this->maxCols) {
                        $moreCalls[] = ['tablecell_open', [1, null, 1], $call[2]];
                        $moreCalls[] = ['cdata', [''], $call[2]];
                        $moreCalls[] = ['tablecell_close', [], $call[2]];
                    }
                    $moreCallsLength = count($moreCalls);
                    if ($moreCallsLength) {
                        array_splice($this->tableCalls, $key, 0, $moreCalls);
                        $key += $moreCallsLength;
                    }

                    if ($this->countTableHeadRows == $lastRow) {
                        array_splice($this->tableCalls, $key + 1, 0, [['tablethead_close', [], $call[2]]]);
                    }
                    break;
            }
        }

        // condense cdata
        $cnt = count($this->tableCalls);
        for ($key = 0; $key < $cnt; $key++) {
            if ($this->tableCalls[$key][0] == 'cdata') {
                $ckey = $key;
                $key++;
                while ($this->tableCalls[$key][0] == 'cdata') {
                    $this->tableCalls[$ckey][1][0] .= $this->tableCalls[$key][1][0];
                    $toDelete[] = $key;
                    $key++;
                }
                continue;
            }
        }

        foreach ($toDelete as $delete) {
            unset($this->tableCalls[$delete]);
        }
        $this->tableCalls = array_values($this->tableCalls);
    }
}