<?php
namespace Site\Lib;

class Markdown {
  private string $content;
  private array $html;
  private string $path;
  private bool $inCodeBlock = false;
  private string $codeBlockLanguage = '';
  private array $codeBlockContent = [];
  private const METADATA_LINE = "----";
  private array $algebraicPlaceholder = [];

  public function __construct(string $path, string $section, ?string $lang = null) {
    $this->html = [];
    if ($lang) $this->path = ROOT.$section.$lang.'/'.$path.'.md';
    else $this->path = ROOT.$section.$path.'.md';

    if (!file_exists($this->path)) {
      header('Location: /404');
      exit();
    }
  }

  /**
   * メタデータを取得する
   * 
   * @return \stdClass  メタデータオブジェクト
   */
  public function getMetadata(): \stdClass {
    $content = file_get_contents($this->path);
    $metadata = new \stdClass();

    $parts = explode(self::METADATA_LINE, $content, 2);
    if (count($parts) < 2) return $metadata;

    $lines = explode("\n", trim($parts[0]));
    foreach ($lines as $line) {
      $line = trim($line);
      if (empty($line)) continue;

      $colonPos = strpos($line, ':');
      if ($colonPos === false) continue;

      $key = trim(substr($line, 0, $colonPos));
      $value = trim(substr($line, $colonPos + 1));
      $value = trim($value, '"\'');

      if ($key == 'category' || $key == 'css') {
        $cat = explode(',', $value);
        $value = $cat;
      }

      $metadata->$key = $value;
    }

    return $metadata;
  }

  /**
   * Markdownをパースする
   * 
   * @return string  HTMLとしてレンダリングされたコンテンツ
   */
  public function parse(): string {
    $content = file_get_contents($this->path);
    $parts = explode(self::METADATA_LINE, $content, 2);
    $this->content = count($parts) > 1 ? trim($parts[1]): trim($content);
    $this->html = [];

    $lines = explode("\n", $this->content);
    $currentParagraph = [];

    $inList = false;
    $listItems = [];
    $listLevel = 0;
    $inBlockquote = false;
    $blockquoteContent = [];
    $tableHeaders = [];
    $tableRows = [];
    $inTable = false;
    $i = 0;

    foreach ($lines as $line) {
      $i++;
      $hasBR = substr($line, -1) === '\\';
      $line = rtrim($line, " \t\r\n\\");

      // コメント
      // if (str_starts_with($line, '//')) {
      //   if ($hasBR && !empty($currentParagraph)) {
      //     $currentParagraph[] = '';
      //   }
      //   continue;
      // }

      // コードブロック
      if (preg_match('/^```(\w*)$/', $line, $matches)) {
        if (!$this->inCodeBlock) {
          if (!empty($currentParagraph)) {
            $this->html[] = "        <p>\n          ".implode("", $currentParagraph)."\n        </p>";
            $currentParagraph = [];
          }
          $this->inCodeBlock = true;
          $this->codeBlockLanguage = $matches[1];
          continue;
        } else {
          $this->html[] = $this->createCodeBlock();
          $this->inCodeBlock = false;
          $this->codeBlockLanguage = '';
          $this->codeBlockContent = [];
          continue;
        }
      }

      if ($this->inCodeBlock) {
        $this->codeBlockContent[] = $line;
        continue;
      }

      // テーブルの処理
      if (preg_match('/^\|(.+)\|$/', $line)) {
        if (!empty($currentParagraph)) {
          $this->html[] = "        <p>\n          ".implode("", $currentParagraph)."\n        </p>";
          $currentParagraph = [];
        }
        $cells = array_map('trim', explode('|', trim($line, '|')));
                
        if (!$inTable) {
          $tableHeaders = $cells;
          $inTable = true;
        } elseif (preg_match('/^\|(\s*:?-+:?\s*\|)+$/', $line)) {
          continue;
        } else {
          $tableRows[] = $cells;
        }
        continue;
      } elseif ($inTable) {
        $this->html[] = $this->createTable($tableHeaders, $tableRows);
        $tableHeaders = [];
        $tableRows = [];
        $inTable = false;
      }

      // 水平線の処理
      if (preg_match('/^([\-\*\_])\1{2,}$/', $line)) {
        if (!empty($currentParagraph)) {
          $this->html[] = "        <p>\n          ".implode("", $currentParagraph)."\n        </p>";
          $currentParagraph = [];
        }
        $this->html[] = "        <hr />\n";
        continue;
      }

      // 引用ブロックの処理
      if (preg_match('/^>\s(.+)/', $line, $matches)) {
        if (!empty($currentParagraph)) {
          $this->html[] = "        <p>\n          ".implode("", $currentParagraph)."\n        </p>";
          $currentParagraph = [];
        }
        $inBlockquote = true;
        $blockquoteContent[] = $this->parseInline($matches[1]);
        continue;
      } elseif ($inBlockquote && empty($line)) {
        $this->html[] = $this->createBlockquote($blockquoteContent);
        $blockquoteContent = [];
        $inBlockquote = false;
        continue;
      }

      // 空行をスキップ
      if (empty($line)) {
        if ($inList) {
          $this->html[] = $this->createList($listItems);
          $listItems = [];
          $inList = false;
        }
        if (!empty($currentParagraph)) {
          $this->html[] = "        <p>\n          ".implode("", $currentParagraph)."\n        </p>";
          $currentParagraph = [];
        }

        continue;
      }

      // ヘッダー
      if (preg_match('/^(#{1,6})\s(.+)/', $line, $m)) {
        if ($inList) {
          $this->html[] = $this->createList($listItems);
          $listItems = [];
          $inList = false;
        }
        if (!empty($currentParagraph)) {
          $this->html[] = "        <p>\n          ".implode("", $currentParagraph)."\n        </p>";
          $currentParagraph = [];
        }

        $level = strlen($m[1]);
        $this->html[] = "        <h{$level}>".$this->parseInline($m[2])."</h{$level}>";
        continue;
      }

      // 箇条書きリスト
      if (preg_match('/^(\s*)([\*\-])\s(.+)/', $line, $m)) {
        if (!empty($currentParagraph)) {
          $this->html[] = "        <p>\n          ".implode("", $currentParagraph)."\n        </p>";
          $currentParagraph = [];
        }
        $inList = true;
        $currentLevel = strlen($m[1]) / 2;
        $listLevel = max($listLevel, $currentLevel);
        $listItems[] = [
          'content' => $this->parseInline($m[3]),
          'level' => $currentLevel,
          'type' => 'ul',
        ];

        continue;
      }

      // 番号付きリスト
      if (preg_match('/^(\s*)\d+\.\s(.+)/', $line, $m)) {
        if (!empty($currentParagraph)) {
          $this->html[] = "        <p>\n          ".implode("", $currentParagraph)."\n        </p>";
          $currentParagraph = [];
        }
        $inList = true;
        $currentLevel = strlen($m[1]) / 2;
        $listLevel = max($listLevel, $currentLevel);
        $listItems[] = [
          'content' => $this->parseInline($m[2]),
          'level' => $currentLevel,
          'type' => 'ol',
        ];

        continue;
      }

      if ($inList) {
        $this->html[] = $this->createList($listItems);
        $listItems = [];
        $inList = false;
        $listLevel = 0;
      }

      $parsedLine = $this->parseInline($line);
      $currentParagraph[] = $parsedLine;
      if ($hasBR) {
        $currentParagraph[] = "<br />\n          ";
      }
    }

    if ($inList) $this->html[] = $this->createList($listItems);
    if ($inBlockquote) $this->html[] = $this->createBlockquote($blockquoteContent);
    if ($inTable) $this->html[] = $this->createTable($tableHeaders, $tableRows);
    if (!empty($currentParagraph))
      $this->html[] = "        <p>\n          ".implode("", $currentParagraph)."\n        </p>";

    return implode("\n", $this->html);
  }

  // 機能性メソッド

  /**
   * インラインのMarkdown記法をパースする
   * 
   * @param string $text  パースするテキスト
   * @return string  HTMLとしてレンダリングされたテキスト
   */
  private function parseInline(string $text): string {
    $this->algebraicPlaceholder = [];
    $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');

    $patterns = [
      // 数式
      '/\$\$([^$]+)\$\$/u' => function($matches): string {
        $placeholder = "{{ALG".count($this->algebraicPlaceholder).'ALG}}';
        $this->algebraicPlaceholder[$placeholder] = $matches[1];
        return $placeholder;
      },
      // 太字
      '/\*\*(.+?)\*\*/u' => '<strong>$1</strong>',
      // 斜体
      '/\*(.+?)\*/u' => '<em>$1</em>',
      // 下線
      '/\_(.+?)\_/u' => '<u>$1</u>',
      // 取り消し線
      '/\~(.+?)\~/u' => '<s>$1</s>',
      // Blink (with speed)
      '/!:\((.+?)\)(.+?):!/u' => '<span class="blink" style="animation-duration: $1s;">$2</span>',
      // Blink
      '/!:(.+?):!/u' => '<span class="blink">$1</span>',
      // フォントの大きさ
      '/\^\((.+?)\)(.+?)\^/u' => '<span style="font-size: $1px;">$2</span>',
      // フォントカラー
      '/\%\((.+?)\)(.+?)\%/u' => '<span style="color: #$1;">$2</span>',
      // 画像
      '/\!\[(.*?)(?:#([^\]]*))?\]\((.+?)\)/u' => '<img style="width: 100%; $2" src="$3" alt="$1" />',
      // 音楽
      '/\$\[([^\]]+)\]\(([^\)]+)\)/u' => '<audio controls><source src="$2" type="$1" /></audio>',
      // 動画
      '/\#\[([^\]]+)\]\(([^\)]+)\)/u' => '<video style="max-width: 100%;" controls><source src="$2" type="$1" /></video>',
      // リンク
      '/\[(.+?)\]\((.+?)\)/u' => '<a href="$2">$1</a>',
      // 振り仮名
      '/\&lt;(.+?)\&gt;\((.+?)\)/u' => '<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>',
      // インラインコード
      '/`(.+?)`/u' => '<code>$1</code>',
    ];

    foreach ($patterns as $pattern => $replacement) {
      if (is_callable($replacement)) {
        $text = preg_replace_callback($pattern, $replacement, $text);
      } else {
        $result = preg_replace($pattern, $replacement, $text);
        if ($result === null) continue;
        $text = $result;
      }
    }

    // プレースホルダーの数式に交換
    foreach ($this->algebraicPlaceholder as $placeholder => $expr) {
      $text = str_replace($placeholder, $this->parseAlgebraic($expr), $text);
    }

    return $text;
  }

  /**
   * 数式記法をパースする
   * 
   * @param string $expression  パースする数式
   * @return string  MathJax用にフォーマットされた数式
   */
  private function parseAlgebraic(string $expression): string {
    // HTMLエスケープ
    $expression = htmlspecialchars_decode($expression, ENT_QUOTES);

    // ふぃりしゃもじのマッピング
    $greekLetters = [
      'alpha' => 'α', 'beta' => 'β', 'gamma' => 'γ', 'delta' => 'δ',
      'epsilon' => 'ε', 'zeta' => 'ζ', 'eta' => 'η', 'theta' => 'θ',
      'iota' => 'ι', 'kappa' => 'κ', 'lambda' => 'λ', 'mu' => 'μ',
      'nu' => 'ν', 'xi' => 'ξ', 'omicron' => 'ο', 'pi' => 'π',
      'rho' => 'ρ', 'sigma' => 'σ', 'tau' => 'τ', 'upsilon' => 'υ',
      'phi' => 'φ', 'chi' => 'χ', 'psi' => 'ψ', 'omega' => 'ω',
      'Alpha' => 'Α', 'Beta' => 'Β', 'Gamma' => 'Γ', 'Delta' => 'Δ',
      'Epsilon' => 'Ε', 'Zeta' => 'Ζ', 'Eta' => 'Η', 'Theta' => 'Θ',
      'Iota' => 'Ι', 'Kappa' => 'Κ', 'Lambda' => 'Λ', 'Mu' => 'Μ',
      'Nu' => 'Ν', 'Xi' => 'Ξ', 'Omicron' => 'Ο', 'Pi' => 'Π',
      'Rho' => 'Ρ', 'Sigma' => 'Σ', 'Tau' => 'Τ', 'Upsilon' => 'Υ',
      'Phi' => 'Φ', 'Chi' => 'Χ', 'Psi' => 'Ψ', 'Omega' => 'Ω',
    ];

    // ギリシャ文字を置換
    foreach ($greekLetters as $text => $symbol) {
      $expression = preg_replace("/\b$text\b/", $symbol, $expression);
    }

    // 上付き文字（例: x^2）
    $expression = preg_replace_callback('/(\w+)\^(\{?(\d+|\w+)\}?)/', function($matches) {
      $base = $matches[1];
      $exponent = $matches[2];
      if ($exponent[0] === '{') $exponent = substr($exponent, 1, -1);

      $superscripts = str_split($exponent);
      $supMap = [
        '0' => '⁰', '1' => '¹', '2' => '²', '3' => '³', '4' => '⁴',
        '5' => '⁵', '6' => '⁶', '7' => '⁷', '8' => '⁸', '9' => '⁹',
        'a' => 'ᵃ', 'b' => 'ᵇ', 'c' => 'ᶜ', 'd' => 'ᵈ', 'e' => 'ᵉ',
        'i' => 'ⁱ', 'n' => 'ⁿ',
      ];
      $converted = '';

      foreach ($superscripts as $char) $converted .= $supMap[$char] ?? $char;

      return $base.'<sup>'.$converted.'</sup>';
    }, $expression);

    // 下付き文字（例: x_1）
    $expression = preg_replace_callback('/(\w+)_((\d+|\w+)\}?)/', function($matches) {
      $base = $matches[1];
      $subscript = $matches[2];

      if ($subscript[0] === '{') $subscript = substr($subscript, 1, -1);

      $subscripts = str_split($subscript);
      $subMap = [
        '0' => '₀', '1' => '₁', '2' => '₂', '3' => '₃', '4' => '₄',
        '5' => '₅', '6' => '₆', '7' => '₇', '8' => '₈', '9' => '₉',
      ];
      $converted = '';

      foreach ($subscripts as $char) {
        $converted .= $subMap[$char] ?? $char;
      }

      return $base.'<sub>'.$converted.'</sub>';
    }, $expression);

    // 分数（例: 4/5x, a/b）
    $expression = preg_replace('/(\d+|\w+)\/(\d*\w+)/',
      '<span class="fraction"><span class="numerator">$1</span><span class="denominator">$2</span></span>',
      $expression);

    // 演算子
    $operators = [
      '\*' => '・',
      '!=' => '≠',
      '>=' => '≥',
      '<=' => '≤',
      'sqrt' => '√',
    ];

    foreach ($operators as $op => $symbol) $expression = preg_replace("/\b$op\b/", $symbol, $expression);

    // 数式全体をspanで囲む
    return '<span class="algebraic">'.$expression.'</span>';
  }

  /**
   * リストを作成する
   * 
   * @param array $items  リストアイテムの配列
   * @param int $maxLevel  最大ネストレベル
   * @return string  HTMLのリスト
   */
  private function createList(array $items, int $maxLevel = 1): string {
    if ($items === []) return '';

    $html = '';
    $currentLevel = 0;
    $listStack = [];
    $currentType = '';

    foreach ($items as $item) {
      $level = isset($item['level']) ? $item['level'] : $currentLevel;

      while ($currentLevel > $level)
        $html .= str_repeat('  ', $currentLevel)."</".array_pop($listStack).">\n";

      while ($currentLevel < $level) {
        $currentLevel++;
        $listStack[] = $item['type'];
        $html .= str_repeat('  ', $currentLevel - 1)."<".$item['type'].">\n";
      }

      if ($currentType != $item['type'] && $currentLevel == $item['level']) {
        if (!empty($listStack)) {
          $html .= str_repeat('  ', $currentLevel)."</".array_pop($listStack).">\n";
          $listStack[] = $item['type'];
          $html .= str_repeat('  ', $currentLevel - 1)."<".$item['type'].">\n";
        }
      }

      $currentType = $item['type'];
    }

    $html .= "        <p>\n          <{$currentType}>\n";
    foreach ($items as $item) {
      $html .= str_repeat('  ', $currentLevel)."            <li>".$item['content']."</li>\n";
    }
    $html .= "          </{$currentType}>\n        </p>";

    while (!empty($listStack)) {
      $html .= str_repeat('  ', $currentLevel)."</".array_pop($listStack).">\n";
      $currentLevel--;
    }

    return rtrim($html);
  }

  /**
   * コードブロックを作成する
   * 
   * @return string  HTMLのコードブロック
   */
  private function createCodeBlock(): string {
    $code = htmlspecialchars(implode("\n", $this->codeBlockContent));
    $class = $this->codeBlockLanguage ? " class=\"language-{$this->codeBlockLanguage}\"" : '';
    return "        <p>\n          <pre><code{$class}>{$code}</code></pre>";
    // $raw   = implode("\n", $this->codeBlockContent);
    // $lang  = $this->codeBlockLanguage ?: 'txt';
    // $class = $lang === 'txt' ? '' : " class=\"language-{$lang}\"";
    // $highlighted = $this->highlightCode($raw, $lang);
    // return "<pre><code{$class}>{$highlighted}</code></pre>";
  }

  private function createBlockquote(array $content): string {
    return "        <p>\n          <blockquote>\n            ".implode("<br />\n            ", $content)."\n          </blockquote>\n        </p>";
  }

  /**
   * テーブルを作成する
   * 
   * @param array $headers  ヘッダー配列
   * @param array $rows  行データの配列
   * @return string  HTMLのテーブル
   */
  private function createTable(array $headers, array $rows): string {
    $html = "        <p>\n          <table>\n";
        
    // ヘッダーを追加
    if (!empty($headers)) {
      $html .= "            <thead>\n              <tr>\n";
      foreach ($headers as $header) {
        $html .= "                <th>".$this->parseInline($header)."</th>\n";
      }
      $html .= "              </tr>\n            </thead>\n";
    }

    // 行を追加
    if (!empty($rows)) {
      $html .= "            <tbody>\n";
      foreach ($rows as $row) {
        $html .= "              <tr>\n";
        foreach ($row as $cell) {
          $html .= "                <td>".$this->parseInline($cell)."</td>\n";
        }
        $html .= "              </tr>\n";
      }
      $html .= "            </tbody>\n";
   }

   $html .= "          </table>\n        </p>";
   return $html;
  }

  /**
   * Pure-PHP syntax highlighter
   *
   * @param string $code   Raw source code
   * @param string $lang   Language identifier (html, css, js, php, …)
   * @return string        HTML with <span class="hl-…"> tokens
   */
  private function highlightCode(string $code, string $lang): string
  {
    $lang = strtolower(trim($lang));
    $rules = [];

    // ---- COMMON -------------------------------------------------------
    $rules['comment'] = [
      // /* … */  (multi-line)
      ['/\/\*(?:.|\n)*?\*\//', 'hl-comment'],
      // // …\n
      ['/\/\/.*(?=\n|$)/', 'hl-comment'],
    ];

    $rules['string'] = [
      // "…"  and '…'  (allow escaped quotes)
      ['/"(?:\\.|[^"\\\n])*"/', 'hl-string'],
      ["/'(?:\\.|[^'\\\n])*'/", 'hl-string'],
    ];

    $rules['number'] = [
      ['/\b(?:0x[0-9a-fA-F]+|0b[01]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b/', 'hl-number'],
    ];

    // ---- HTML ---------------------------------------------------------
    if ($lang === 'html') {
      $rules['tag']   = [['/&lt;\/?[a-zA-Z0-9\-]+/', 'hl-tag']];
      $rules['attr']  = [['/\s[a-zA-Z0-9\-]+(?==)/', 'hl-attr']];
      $rules['value'] = [['/(?<==)"(?:\\.|[^"\\\n])*"(?=[>\s])/', 'hl-value']];
    }

    // ---- CSS ----------------------------------------------------------
    if ($lang === 'css') {
      $rules['selector'] = [['/^[\t ]*[a-zA-Z0-9#\.\-\:$$  $$\=\*\>\+\~\^]+(?=\s*\{)/m', 'hl-selector']];
      $rules['property'] = [['/\b[a-z\-]+(?=\s*:)/', 'hl-property']];
      $rules['value']    = [['/(?<=\:)\s*[^;]+(?=;)/', 'hl-value']];
      $rules['unit']     = [['/\b\d+(px|em|rem|%|vh|vw|pt|pc|cm|mm|in)\b/i', 'hl-unit']];
    }

    // ---- PHP ----------------------------------------------------------
    if ($lang === 'php') {
      $keywords = 'abstract|and|array|as|break|callable|case|catch|class|clone|'
                . 'const|continue|declare|default|die|do|echo|else|elseif|empty|'
                . 'enddeclare|endfor|endforeach|endif|endswitch|endwhile|eval|exit|'
                . 'extends|final|finally|for|foreach|function|global|goto|if|'
                . 'implements|include|include_once|instanceof|insteadof|interface|'
                . 'isset|list|namespace|new|or|print|private|protected|public|'
                . 'require|require_once|return|static|switch|throw|trait|try|unset|'
                . 'use|var|while|xor|yield';
      $rules['keyword'] = [['/\b(?:' . $keywords . ')\b/', 'hl-keyword']];
      $rules['function']= [['/\b([a-zA-Z_\x7f-\xff][\w\x7f-\xff]*)(?=\s*\()/', 'hl-function']];
      $rules['variable']= [['/\$[a-zA-Z_\x7f-\xff][\w\x7f-\xff]*/', 'hl-variable']];
    }

    // ---- Shell --------------------------------------------------------
    if ($lang === 'sh') {
      $keywords = 'if|then|else|elif|fi|case|esac|for|select|while|until|do|done|in';
      $rules['keyword'] = [['/\b(?:' . $keywords . ')\b/', 'hl-keyword']];
      $rules['variable']= [['/\$\{?[a-zA-Z_][\w]*\}?/', 'hl-variable']];
    }

    // ---- JSON ---------------------------------------------------------
    if ($lang === 'json') {
      $rules['property'] = [['/"([^"]+)":/', '<span class="hl-property">$1</span>:']];
    }

    // ---- C / C++ -----------------------------------------------
    if (in_array($lang, ['c', 'cpp', 'cs'])) {
      $keywords = 'abstract|assert|boolean|break|byte|case|catch|char|class|const|'
                . 'continue|default|do|double|else|enum|extends|false|final|finally|'
                . 'float|for|goto|if|implements|import|instanceof|int|interface|long|'
                . 'native|new|null|package|private|protected|public|return|short|static|'
                . 'strictfp|super|switch|synchronized|this|throw|throws|transient|true|'
                . 'try|void|volatile|while';
      $rules['keyword'] = [['/\b(?:' . $keywords . ')\b/', 'hl-keyword']];
      $rules['type']    = [['/\b(?:int|float|double|char|bool|void|string)\b/', 'hl-type']];
      $rules['define']  = [['/(#include)|(#define)/', 'hl-define']];
    }

    if ($lang === 'markdown') {
      $rules['header'] = [['/^(#{1,6})\s+(.+)$/', '<span class="hl-header">$1 $2</span>']];
      $rules['link']   = [['/\[([^\]]+)\]\(([^)]+) $$/', '<span class="hl-link">[$1]($2)</span>']];
    }

    $html = htmlspecialchars($code, ENT_NOQUOTES, 'UTF-8');

    $order = ['comment', 'string', 'number'];
    foreach (['tag','attr','value','selector','property','unit','keyword','function','variable','type','header','link'] as $k) {
      if (isset($rules[$k])) $order[] = $k;
    }

    foreach ($order as $type) {
      if (!isset($rules[$type])) continue;
      foreach ($rules[$type] as $rule) {
        [$pattern, $class] = $rule;
        $html = preg_replace_callback(
          $pattern,
          function ($m) use ($class) {
            // For JSON property we already built the <span>
            if (strpos($m[0], '<span') === 0) return $m[0];
            $inner = $m[0];
            // Preserve already escaped entities inside strings/comments
            return '<span class="'.$class.'">'.$inner.'</span>';
          },
          $html
        );
      }
    }

    return $html;
  }
}