# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Modify the CMakeLists.txt from LevelDb to statically link Snappy compression
support.
"""

import argparse
import dataclasses
import os
import pathlib
import subprocess
from typing import Iterable, Sequence


def main() -> None:
  arg_parser = argparse.ArgumentParser()
  arg_parser.add_argument("--snappy-source-dir", required=True)
  arg_parser.add_argument("--snappy-binary-dir", required=True)
  arg_parser.add_argument("--additional-patch-file", required=False)
  parsed_args = arg_parser.parse_args()
  del arg_parser
  snappy_source_dir = pathlib.Path(parsed_args.snappy_source_dir)
  snappy_binary_dir = pathlib.Path(parsed_args.snappy_binary_dir)
  additional_patch_file = parsed_args.additional_patch_file
  del parsed_args

  cmakelists_txt_file = pathlib.Path("CMakeLists.txt")
  with cmakelists_txt_file.open("rt", encoding="utf8") as f:
    lines = tuple(f)

  patcher = CMakeListsPatcher(
    lines,
    os.path.abspath(__file__),
    snappy_source_dir,
    snappy_binary_dir,
  )

  patched_lines = tuple(patcher.patch())

  with cmakelists_txt_file.open("wt", encoding="utf8") as f:
    f.writelines(patched_lines)

  additional_patch_stamp = 'leveldb_additional_patch_stamp'
  if additional_patch_file and not os.path.exists(additional_patch_stamp):
    print("Applying patch %s" % additional_patch_file)
    subprocess.run(['git', 'apply', '-v', additional_patch_file],
                   check=True)
    # Create a stamp file so the patch isn't applied twice.
    open(additional_patch_stamp, 'a').close()


@dataclasses.dataclass(frozen=True)
class LineComponents:
  full: str
  indent: str
  line: str
  eol: str


class CMakeListsPatcher:

  SNAPPY_DETECT_LINE = \
    """check_library_exists(snappy snappy_compress "" HAVE_SNAPPY)"""
  SNAPPY_INCLUDE_LINE = \
    "target_include_directories(leveldb"
  SNAPPY_LINK_LINE = \
    "target_link_libraries(leveldb snappy)"

  def __init__(
      self,
      lines: Sequence[str],
      script_name: str,
      snappy_source_dir: pathlib.Path,
      snappy_binary_dir: pathlib.Path) -> None:
    self.i = 0
    self.lines = lines
    self.script_name = script_name
    self.snappy_source_dir_str = snappy_source_dir.as_posix()
    self.snappy_binary_dir_str = snappy_binary_dir.as_posix()

  def patch(self) -> Iterable[str]:
    while self.i < len(self.lines):
      full_line = self.lines[self.i]
      line = self._split_line(full_line)
      self.i += 1

      if line.line == self.SNAPPY_DETECT_LINE:
        yield from self._on_snappy_detect_line(line)
      elif line.line == self.SNAPPY_INCLUDE_LINE:
        yield full_line
        yield from self._on_leveldb_include_start()
      elif line.line == self.SNAPPY_LINK_LINE:
        yield from self._on_leveldb_snappy_link_line(line)
      else:
        yield full_line

  def _begin_mod_line(self, mod_name: str) -> str:
    return f"# BEGIN: {mod_name} modification by {self.script_name}"

  def _end_mod_line(self, mod_name: str) -> str:
    return f"# END: {mod_name} modification by {self.script_name}"

  def _on_snappy_detect_line(self, line: LineComponents) -> Iterable[str]:
    yield self._begin_mod_line("snappy_detect_line") + line.eol
    yield line.indent + "# " + line.line + line.eol
    yield line.indent + """set(HAVE_SNAPPY ON CACHE BOOL "")""" + line.eol
    yield self._end_mod_line("snappy_detect_line") + line.eol

  def _on_leveldb_include_start(self) -> Iterable[str]:
    line1 = self._split_line(self.lines[self.i])
    line2 = self._split_line(self.lines[self.i + 1])
    begin_mod_line = self._begin_mod_line("leveldb_include_start")

    if line1.line == begin_mod_line:
      return

    yield begin_mod_line + line1.eol
    yield line1.indent + "PRIVATE" + line1.eol
    yield line2.indent + self.snappy_source_dir_str + line2.eol
    yield line2.indent + self.snappy_binary_dir_str + line2.eol
    yield self._end_mod_line("leveldb_include_start") + line1.eol

  def _on_leveldb_snappy_link_line(self, line: LineComponents) -> Iterable[str]:
    yield self._begin_mod_line("leveldb_snappy_link_line") + line.eol
    yield line.indent + "# " + line.line + line.eol
    yield line.indent + f"target_link_libraries(leveldb Snappy::Snappy)" + line.eol
    yield self._end_mod_line("leveldb_snappy_link_line") + line.eol

  def _split_line(self, line: str) -> LineComponents:
    line_rstripped = line.rstrip()
    eol = line[len(line_rstripped):]
    line_stripped = line_rstripped.strip()
    indent = line_rstripped[:len(line_rstripped) - len(line_stripped)]
    return LineComponents(full=line, indent=indent, line=line_stripped, eol=eol)


class LeveDbPatchException(Exception):
  pass


if __name__ == "__main__":
  main()
