# Copyright 2024 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.

import os
import re
import waflib
from waflib import Utils
from waflib.Configure import conf

def find_clang_path(conf):
  """ Find the first clang on our path with a version greater than 3.2"""

  out = conf.cmd_and_log('which -a clang')
  paths = out.splitlines()
  for path in paths:
    # Make sure clang is at least version 3.3
    out = conf.cmd_and_log('%s --version' % path)
    r = re.findall(r'clang version (\d+)\.(\d+)', out)
    if len(r):
      version_major = int(r[0][0])
      version_minor = int(r[0][1])
      if version_major > 3 or (version_major == 3 and version_minor >= 3):
        return path

  conf.fatal('No version of clang 3.3+ found on your path!')

def find_toolchain_path(conf):
  possible_paths = ['~/arm-cs-tools/arm-none-eabi',
                    '/usr/local/Cellar/arm-none-eabi-gcc/arm/arm-none-eabi']
  for p in possible_paths:
      if os.path.isdir(p):
          return p

  conf.fatal('could not find arm-none-eabi folder')

def find_sysroot_path(conf):
  """ The sysroot is a directory struct that looks like /usr/bin/ that includes custom
      headers and libraries for a target. We want to use the headers from our mentor
      toolchain, but they don't have a structure like /usr/bin. Therefore, if this is
      first time configuring on a system, create the directory structure with the
      appropriate symlinks so things work out.

      Note that this is a bit of a hack. Ideally our toolchain setup script will produce
      the directory structure that we expect, but I'm not going to muck with the toolchain
      too much until I get everything working end to end as opposed to having to rebuild
      our toolchain all the time. """

  toolchain_path = find_toolchain_path(conf)

  sysroot_path = os.path.join(toolchain_path, 'sysroot')
  if not os.path.isdir(sysroot_path):
    waflib.Logs.pprint('CYAN', 'Sysroot dir not found at %s, creating...', sysroot_path)
    os.makedirs(os.path.join(sysroot_path, 'usr/local/'))

    os.symlink(os.path.join(toolchain_path, 'include/'),
               os.path.join(sysroot_path, 'usr/local/include'))

  return sysroot_path

@conf
def using_clang_compiler(ctx):
    compiler_name = ctx.env.CC
    if isinstance(ctx.env.CC, list):
        compiler_name = ctx.env.CC[0]

    if 'CCC_CC' in os.environ:
        compiler_name = os.environ['CCC_CC']

    return 'clang' in compiler_name


def options(opt):
    opt.add_option('--relax_toolchain_restrictions', action='store_true',
                   help='Allow us to compile with a non-standard toolchain')
    opt.add_option('--use_clang', action='store_true',
                   help='(EXPERIMENTAL) Uses clang instead of gcc as our compiler')
    opt.add_option('--use_env_cc', action='store_true',
                   help='Use whatever CC is in the environment as our compiler')
    opt.add_option('--beta', action='store_true',
                   help='Build in beta mode '
                        '(--beta and --release are mutually exclusive)')
    opt.add_option('--release', action='store_true',
                   help='Build in release mode'
                        ' (--beta and --release are mutually exclusive)')
    opt.add_option('--fat_firmware', action='store_true',
                   help='build in GDB mode WITH logs; requires 1M of onbaord flash')
    opt.add_option('--gdb', action='store_true',
                   help='build in GDB mode (no optimization, no logs)')
    opt.add_option('--lto', action='store_true', help='Enable link-time optimization')
    opt.add_option('--no-lto', action='store_true', help='Disable link-time optimization')
    opt.add_option('--save_temps', action='store_true',
                   help='Save *.i and *.s files during compilation')
    opt.add_option('--no_debug', action='store_true',
                   help='Remove -g debug information. See --save_temps')

def configure(conf):
    CROSS_COMPILE_PREFIX = 'arm-none-eabi-'

    conf.env.AS = CROSS_COMPILE_PREFIX + 'gcc'
    conf.env.AR = CROSS_COMPILE_PREFIX + 'gcc-ar'
    if conf.options.use_env_cc:
      pass # Don't touch conf.env.CC
    elif conf.options.use_clang:
      conf.env.CC = find_clang_path(conf)
    else:
      conf.env.CC = CROSS_COMPILE_PREFIX + 'gcc'

    conf.env.LINK_CC = conf.env.CC

    conf.load('gcc')

    conf.env.append_value('CFLAGS', [ '-std=c11', ])

    c_warnings = [ '-Wall',
                   '-Wextra',
                   '-Werror',
                   '-Wpointer-arith',
                   '-Wno-unused-parameter',
                   '-Wno-missing-field-initializers',
                   '-Wno-error=unused-function',
                   '-Wno-error=unused-variable',
                   '-Wno-error=unused-parameter' ]

    if conf.using_clang_compiler():
      sysroot_path = find_sysroot_path(conf)

      # Disable clang warnings from now... they don't quite match
      c_warnings = []

      conf.env.append_value('CFLAGS', [ '-target', 'arm-none-eabi' ])
      conf.env.append_value('CFLAGS', [ '--sysroot', sysroot_path ])

      # Clang doesn't enable short-enums by default since
      # arm-none-eabi is an unsupported target
      conf.env.append_value('CFLAGS', '-fshort-enums')

      arm_toolchain_path = find_toolchain_path(conf)
      conf.env.append_value('CFLAGS', [ '-B' + arm_toolchain_path ])

      conf.env.append_value('LINKFLAGS', [ '-target', 'arm-none-eabi' ])
      conf.env.append_value('LINKFLAGS', [ '--sysroot', sysroot_path ])

    else:
      # These warnings only exist in GCC
      c_warnings.append('-Wno-error=unused-but-set-variable')
      c_warnings.append('-Wno-packed-bitfield-compat')

      if not ('4', '8') <= conf.env.CC_VERSION <= ('4', '9', '3'):
        # Verify the toolchain we're using is allowed. This is to prevent us from accidentally
        # building and releasing firmwares that are built in ways we haven't tested.

        if not conf.options.relax_toolchain_restrictions:
            TOOLCHAIN_ERROR_MSG = \
"""=== INVALID TOOLCHAIN ===
Either upgrade your toolchain using the process listed here:
    https://pebbletechnology.atlassian.net/wiki/display/DEV/Firmware+Toolchain
Or re-configure with the --relax_toolchain_restrictions option. """

            conf.fatal('Invalid toolchain detected!\n' + \
                       repr(conf.env.CC_VERSION) + '\n' + \
                       TOOLCHAIN_ERROR_MSG)

    conf.env.CFLAGS.append('-I' + conf.path.abspath() + '/src/fw/util/time')

    conf.env.append_value('CFLAGS', c_warnings)

    conf.add_platform_defines(conf.env)

    conf.env.ASFLAGS = [ '-xassembler-with-cpp', '-c' ]
    conf.env.AS_TGT_F = '-o'

    conf.env.append_value('LINKFLAGS', [ '-Wl,--warn-common' ])

    args = [ '-fvar-tracking-assignments',  # Track variable locations better
             '-mthumb',
             '-ffreestanding',
             '-ffunction-sections',
             '-fbuiltin',
             '-fno-builtin-itoa' ]

    if not conf.options.no_debug:
        args += [ '-g3',  # Extra debugging info, including macro definitions
                  '-gdwarf-4' ] # More detailed debug info

    if conf.options.save_temps:
        args += [ '-save-temps=obj' ]

    if conf.options.lto:
        args += [ '-flto' ]
        if not using_clang_compiler(conf):
            # None of these options are supported by clang
            args += [ '-flto-partition=balanced',
                      '--param','lto-partitions=128', # Can be trimmed down later
                      '-fuse-linker-plugin',
                      '-fno-if-conversion',
                      '-fno-caller-saves',
                      '-fira-region=mixed',
                      '-finline-functions',
                      '-fconserve-stack',
                      '--param','inline-unit-growth=1',
                      '--param','max-inline-insns-auto=1',
                      '--param','max-cse-path-length=1000',
                      '--param','max-grow-copy-bb-insns=1',
                      '-fno-hoist-adjacent-loads',
                      '-fno-optimize-sibling-calls',
                      '-fno-schedule-insns2' ]

    cpu_fpu = None
    if conf.env.MICRO_FAMILY == "STM32F2":
        args += [ '-mcpu=cortex-m3' ]
    elif conf.env.MICRO_FAMILY == "STM32F4":
        args += [ '-mcpu=cortex-m4']
        cpu_fpu = "fpv4-sp-d16"
    elif conf.env.MICRO_FAMILY == "STM32F7":
        args += [ '-mcpu=cortex-m7']
        cpu_fpu = "fpv5-d16"
    # QEMU does not have FPU
    if conf.env.QEMU:
        cpu_fpu = None

    if cpu_fpu:
      args += [ "-mfloat-abi=softfp",
                "-mfpu="+cpu_fpu ]
    else:
      # Not using float-abi=softfp means no FPU instructions.
      # It also defines __SOFTFP__=1
      # Yes that define name is super misleading, but what can you do.
      pass

    conf.env.append_value('CFLAGS', args)
    conf.env.append_value('ASFLAGS', args)
    conf.env.append_value('LINKFLAGS', args)

    conf.env.SHLIB_MARKER = None
    conf.env.STLIB_MARKER = None

    # Set whether or not we show the "Your Pebble just reset..." alert
    if conf.options.release and conf.options.beta:
      raise RuntimeError("--beta and --release are mutually exclusive and cannot be used together")
    if not conf.options.release:
      conf.env.append_value('DEFINES', [ 'SHOW_PEBBLE_JUST_RESET_ALERT' ])
      conf.env.append_value('DEFINES', [ 'SHOW_BAD_BT_STATE_ALERT' ])
      if not conf.is_bigboard():
        conf.env.append_value('DEFINES', [ 'SHOW_ACTIVITY_DEMO' ])

    # Set optimization level
    if conf.options.beta:
        optimize_flags = '-Os'
        print "Beta mode"
    elif conf.options.release:
        optimize_flags = '-Os'
        print "Release mode"
    elif conf.options.fat_firmware:
        optimize_flags = '-O0'
        conf.env.IS_FAT_FIRMWARE = True
        print 'Building Fat Firmware (no optimizations, logging enabled)'
    elif conf.options.gdb:
        optimize_flags = '-Og'
        print "GDB mode"
    else:
        optimize_flags = '-Os'
        print 'Debug Mode'

    conf.env.append_value('CFLAGS', optimize_flags)
    conf.env.append_value('LINKFLAGS', optimize_flags)