<?php

namespace Openguru\OpenCartFramework\QueryBuilder\Grammars;

use BadMethodCallException;
use Openguru\OpenCartFramework\QueryBuilder\Builder;
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
use Openguru\OpenCartFramework\QueryBuilder\RawExpression;

abstract class Grammar
{
    protected $compiled = [
        'columns' => [],
        'from' => [],
        'joins' => [],
        'wheres' => [],
        'orders' => [],
        'limit' => [],
        'offset' => [],
        'groupBy' => [],
    ];

    private function resetCompiled(): void
    {
        $this->compiled = [
            'columns' => [],
            'from' => [],
            'joins' => [],
            'wheres' => [],
            'orders' => [],
            'limit' => [],
            'offset' => [],
            'groupBy' => [],
        ];
    }

    public function compileComponents(Builder $builder): string
    {
        $this->resetCompiled();

        foreach (array_keys($this->compiled) as $component) {
            $method = 'compile' . ucfirst($component);

            if (!method_exists($this, $method)) {
                throw new BadMethodCallException("Method '{$method}' does not exist.");
            }

            if (! empty($builder->{$component})) {
                $this->compiled[$component] = $this->{$method}(
                    $builder,
                    $builder->{$component},
                );
            }
        }

        return $this->concatCompiled($this->compiled);
    }

    public function concatCompiled(array $compiled): string
    {
        $array = [];

        foreach (array_keys($this->compiled) as $component) {
            $array[] = $compiled[$component] ?? null;
        }

        return implode(' ', array_filter($array));
    }

    public function compileColumns(Builder $builder, $columns): string
    {
        if ($columns === '*' || count($columns) === 0) {
            return 'SELECT *';
        }

        $columns = array_map(static function ($item) {
            if ($item instanceof RawExpression) {
                return $item->getValue();
            }

            return $item['column'] . ($item['as'] !== null ? ' AS ' . $item['as'] : '');
        }, $columns);

        $distinct = $builder->distinct ? 'DISTINCT ' : '';

        return "SELECT {$distinct}" . implode(', ', $columns);
    }

    public function compileWheres(Builder $builder, $wheres, string $prefix = 'WHERE '): string
    {
        if (!$wheres) {
            return '';
        }

        $compiledConditions = [];

        $counter = 0;
        foreach ($wheres as $condition) {
            $compiledConditions[] = ($counter > 0 ? mb_strtoupper($condition['boolean']) . ' ' : '')
                . $this->{'where' . $condition['type']}($condition);
            $counter++;
        }

        return $prefix . implode(' ', $compiledConditions);
    }

    private function getRawValue($value): string
    {
        if ($value instanceof RawExpression) {
            return $value->getValue();
        }

        return $value;
    }

    public function whereBasic($condition): string
    {
        return $this->getRawValue($condition['column']) . ' ' . $condition['operator'] . ' ?';
    }

    public function whereRaw($condition): string
    {
        return $condition['expression'];
    }

    public function whereNull($condition): string
    {
        return $this->getRawValue($condition['column']) . ' IS NULL';
    }

    public function whereNotNull($condition): string
    {
        return $this->getRawValue($condition['column']) . ' IS NOT NULL';
    }

    public function whereNested($condition): string
    {
        return '(' . $this->compileWheres($condition['query'], $condition['query']->wheres, '') . ')';
    }

    public function whereBetween($condition): string
    {
        return $this->getRawValue($condition['column']) . ' BETWEEN ? AND ?';
    }

    public function compileOrders(Builder $builder, array $orders): string
    {
        return 'ORDER BY ' . implode(
            ', ',
            array_map(
                static function ($order) {
                        return $order['column'] . ' ' . $order['direction'];
                },
                $orders
            )
        );
    }

    public function compileFrom(Builder $builder, array $from): string
    {
        return 'FROM ' . $from['table'] . ($from['as'] !== null ? ' AS ' . $from['as'] : '');
    }

    public function compileLimit(Builder $builder, $limit): string
    {
        return "LIMIT $limit";
    }

    public function compileOffset(Builder $builder, $offset): string
    {
        return "OFFSET $offset";
    }

    public function compileJoins(Builder $builder, $joins): string
    {
        return implode(' ', array_map(function (JoinClause $join) use ($builder) {
            $conditions = '';
            if ($join->wheres) {
                $conditions = ' AND ' . $this->compileWheres($builder, $join->wheres, '');
            }

            return mb_strtoupper(
                $join->type
            ) . " JOIN $join->table ON $join->first $join->operator $join->second" . $conditions;
        }, $joins));
    }

    public function whereIn($condition): string
    {
        if (! $condition['value']) {
            return '';
        }

        $inValues = str_repeat('?, ', count($condition['value']) - 1) . '?';
        $inOperator = $condition['operator'];

        return $this->getRawValue($condition['column']) . " $inOperator (" . $inValues . ')';
    }

    public function compileGroupBy(Builder $builder, array $groupBy): string
    {
        return 'GROUP BY ' . implode(', ', $groupBy);
    }

    public function compileUpdate(Builder $builder, string $table, array $values): string
    {
        $columns = [];

        foreach ($values as $key => $value) {
            $columns[] = "`{$key}` = ?";
        }

        $columns = implode(', ', $columns);

        $joins = $this->compileJoins($builder, $builder->joins);

        $joins = $joins ? ' ' . $joins : '';

        $wheres = $this->compileWheres($builder, $builder->wheres);

        $wheres = $wheres ? ' ' . $wheres : '';

        return "UPDATE `{$table}`{$joins} SET {$columns}{$wheres}";
    }
}
