# This file is NOT licensed under the GPLv3, which is the license for the rest
# of ycmd.
#
# Here's the license text for this file:
#
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
#
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# For more information, please refer to <http://unlicense.org/>

import os

from ycmd import utils
import ycm_core

# Overview
# --------
#
# This file provides an attempt to heuristically provide compiler flags for
# c-family languages. It works best when a compilation database is used.  A
# compilation database can be generated by tools such as cmake, bear, etc. It
# contains the compiler flags (actually, a compiler invocation) for each source
# file in the project, generated by the make system. If no compilation database
# is used, an attempt is made to guess some flags.
#
# Finding the compilation database
# --------------------------------
#
# While there is no standard, and using something like cmake typically involves
# an out-of-tree build, we attempt to find a compilation database by walking the
# directory hierarchy looking for a file named compile_commands.json, starting
# at the path of the file for which flags were requested (i.e. the file being
# edited).
#
# If a compilation database is found, it is cached against the directory it is
# found in, to prevent re-parsing the entire database for every file, while
# allowing a single instance ycmd to operate on multiple "projects" at once.
#
# Guessing flags
# --------------
#
# The compilation database typically only contains flags (compiler invocations)
# for source files known to the make system and/or makefile generator, as these
# form the translation units for which calls to the compiler (typically with the
# -c flag) are actually made. It does not contain:
#  - new files, not yet run through the make generator system
#  - header files
#
# Additionally, not all projects will have a compilation database. While it is
# recommended to generate one (again, see 'bear' if you don't use cmake), we try
# to make certain toy or example projects work without user input.
#
# So the logic in this file applies some simple heuristics to guess the flags in
# those cases:
#  - If the file is a header file (according to HEADER_EXTENSIONS)
#    above, see if there is an entry in the database with the same root, but
#    with any of the extensions in SOURCE_EXTENSIONS above. If so, return those
#    flags.
#  - Otherwise, if there is an entry in the database for the requested file,
#    return those flags.
#  - Otherwise, if we previously found any flags for a file in the same
#    directory as the requested file, return those flags.
#  - Otherwise, if we previously returned *any* flags for *any* file with the
#    same file extension, return those.
#  - Otherwise, if the file extension is known to us (c++, c, objective c), we
#    return some absolutely arbitrary default flags, which might work for toy
#    projects.
#  - Otherwise, we really can't guess any flags, so return none (and thus a
#    warning that we can't guess your flags).
#
# Caution
# -------
#
# These heuristics might not work for you! Your mileage may vary. It is provided
# as a default in the hope that it might be useful for some number of users.
#
# The file is deliberately heavy on comments. This is to allow it to be adapted
# to individual use cases by copying it to a known directory, configuring it
# as the 'global_extra_conf' and customising it to your particular
# environment. Note that the comments in this file are not a replacement for the
# ycmd or YouCompleteMe documentation, nor is is meant to be a replacement for
# the comments/examples found in ycmd's own C++ source location
# (../cpp/ycm/.ycm_extra_conf.py).
#

# Tweakables for heuristics {{{

# List of file extensions to be considered "header" files and thus not present
# in the compilation database. The logic will try and find an associated
# "source" file (see SOURCE_EXTENSIONS below) and use the flags for that.
HEADER_EXTENSIONS = [ '.h', '.hxx', '.hpp', '.hh' ]

# List of file extensions which are considered "source" files for the purposes
# of heuristically locating the flags for a header file.
SOURCE_EXTENSIONS = [ '.cpp', '.cxx', '.cc', '.c', '.m', '.mm' ]

# }}}

# Caches for heuristically guessing flags {{{

# We cache the database for any given source directory
compilation_database_dir_map = {}

# Sometimes we don't actually know what the flags to use are. Rather than
# returning no flags, if we've previously found flags for a file in a particular
# directory, return them. The will probably work in a high percentage of cases
# and allow new files (which are not yet in the compilation database) to receive
# at least some flags
file_directory_heuristic_map = {}

# Assuming we can't find anything in the database, and we haven't previously
# seen any compilation information for the directory of the file in question, we
# simply return the last set of flags we successfully retrieved for any file
# sharing the file extension. This might work for some percentage of files,
# which is better than the 0% otherwise.
last_compilation_info_ext_map = {}

# As a last gambit, we just return some generic flags. This won't work for
# any large or complex project, but it might work for trivial/toy projects,
# demos, school projects etc.

# This map contains the flag lists to return for the supported file types. The
# flags are arbitrary, but should work for a large number of toy projects (when
# combined with the other os-specific flags added by ycmd).
fallback_flags_filetype_map = {
  'cpp':  [
    '-x', 'c++',
    '-std=c++11',
    '-Wall',
    '-Wextra',
    '-I', '.'
  ],
  'c': [
    '-x', 'c',
    '-std=c99',
    '-Wall',
    '-Wextra',
    '-I', '.'
  ],
  'objc': [
    '-x', 'objective-c',
    '-Wall',
    '-Wextra',
    '-I', '.'
  ],
}

# This map contains the mapping of file extension (including the .) to the file
# type we expect it to be. We would prefer to rely on the editor to tell us the
# file type, but we can't because there is no way to force that to happen.
fallback_flags_ext_map = {
  '.cpp': fallback_flags_filetype_map[ 'cpp' ],
  '.cxx': fallback_flags_filetype_map[ 'cpp' ],
  '.cc':  fallback_flags_filetype_map[ 'cpp' ],
  '.c':   fallback_flags_filetype_map[ 'c' ],
  '.m':   fallback_flags_filetype_map[ 'objc' ],
  '.mm':  fallback_flags_filetype_map[ 'objc' ], # sic: strictly obj-c++ ?
}

# }}}

# Implementation {{{


# Return a compilation database object for the supplied path or None if none
# could be found.
#
# We search up the directory hierarchy, to first see if we have a compilation
# database already for that path, or if a compile_commands.json file exists in
# that directory.
def FindCompilationDatabase( wd ):
  # Find all the ancestor directories of the supplied directory
  for folder in utils.PathsToAllParentFolders( wd ):
    # Did we already cache a database for this path?
    if folder in compilation_database_dir_map:
      # Yep. Return that.
      return compilation_database_dir_map[ folder ]

    # Guess not. Let's see if a compile_commands.json file already exists...
    compile_commands = os.path.join( folder, 'compile_commands.json' )
    if os.path.exists( compile_commands ):
      # Yes, it exists. Create a database and cache it in our map.
      database = ycm_core.CompilationDatabase( folder )
      compilation_database_dir_map[ folder ] = database
      return database

    # Doesn't exist. Check the next ancestor

  # Nothing was found. No compilation flags are available.
  #
  # Note: we cache the fact that none was found for this folder to speed up
  # subsequent searches..
  compilation_database_dir_map[ wd ] = None
  return None


def HeaderPathManipulations( file_name ):
  yield file_name

  # Try some simple manipulations, like removing "include" from the path and
  # replacing with "src" or "lib" (as used by llvm)

  include_mappings = [
    [ '/include/', '/src/' ],
    [ '/include/', '/lib/' ],
    [ '/include/clang/', '/lib/' ],
  ]

  for mapping in include_mappings:
    if mapping[ 0 ] in file_name:
      yield file_name.replace( mapping[ 0 ], mapping[ 1 ], 1 )


# Find the compilation info structure from the supplied database for the
# supplied file. If the source file is a header, try and find an appropriate
# source file and return the compilation_info for that.
def GetCompilationInfoForFile( database, file_name, file_extension ):

  # The compilation_commands.json file generated by CMake does not have entries
  # for header files. So we do our best by asking the db for flags for a
  # corresponding source file, if any. If one exists, the flags for that file
  # should be good enough.
  if file_extension in HEADER_EXTENSIONS:
    # It's a header file
    for candidate_file in HeaderPathManipulations( file_name ):
      for extension in SOURCE_EXTENSIONS:
        replacement_file = os.path.splitext( candidate_file )[ 0 ] + extension
        if os.path.exists( replacement_file ):
          # We found a corresponding source file with the same file_root. Try
          # and get the flags for that file.
          compilation_info = database.GetCompilationInfoForFile(
            replacement_file )
          if compilation_info.compiler_flags_:
            return compilation_info

    # No corresponding source file was found, so we can't generate any flags for
    # this header file.
    return None

  # It's a source file. Just ask the database for the flags.
  compilation_info = database.GetCompilationInfoForFile( file_name )
  if compilation_info.compiler_flags_:
    return compilation_info

  return None


# In the absence of flags from a compilation database or heuristic, return some
# generic flags, or None if we don't have any generic flags for the supplied
# file extension.
def GetDefaultFlagsForFile( extension ):
  if extension in fallback_flags_ext_map:
    return {
      'flags':    fallback_flags_ext_map[ extension ],
      'do_cache': True
    }
  else:
    # OK we really have no clue about this filetype and we can't find any
    # flags, even with outrageous guessing. Return nothing and warn the user
    # that we couldn't find or guess any compiler flags.
    return {
      'flags': []
    }


def MakeRelativePathsInFlagsAbsolute( flags, working_directory ):
  if not working_directory:
    return list( flags )
  new_flags = []
  make_next_absolute = False
  path_flags = [ '-isystem', '-I', '-iquote', '--sysroot=' ]
  for flag in flags:
    new_flag = flag

    if make_next_absolute:
      make_next_absolute = False
      if not flag.startswith( '/' ):
        new_flag = os.path.join( working_directory, flag )

    for path_flag in path_flags:
      if flag == path_flag:
        make_next_absolute = True
        break

      if flag.startswith( path_flag ):
        path = flag[ len( path_flag ): ]
        new_flag = path_flag + os.path.join( working_directory, path )
        break

    if new_flag:
      new_flags.append( new_flag )
  return new_flags


# ycmd calls this method to get the compile flags for a given file. It returns a
# dictionary with 2 keys: 'flags' and 'do_cache', or None if no flags can be
# found.
def FlagsForFile( file_name, **kwargs ):
  file_dir = os.path.dirname( file_name )
  ( file_root, file_extension ) = os.path.splitext( file_name )

  # Create or retrieve the cached compilation database object
  database = FindCompilationDatabase( file_dir )
  if database is None:
    # We couldn't find a compilation database. Just return some absolutely
    # generic flags based on the file extension.
    #
    # We don't bother cacheing this as that would just be a waste of memory and
    # cycles.
    return GetDefaultFlagsForFile( file_extension )

  compilation_info = GetCompilationInfoForFile( database,
                                                file_name,
                                                file_extension )

  if compilation_info is None:
    print( "No flags in database for " + file_name )
    if file_dir in file_directory_heuristic_map:
      # We previously saw a file in this directory. As a guess, just
      # return the flags for that file. Hopefully this will at least give some
      # meaningful suggestions
      print( " - Using flags for dir: " + file_dir )
      compilation_info = file_directory_heuristic_map[ file_dir ]
    elif file_extension in last_compilation_info_ext_map:
      # OK there is nothing in the DB for this file, and we didn't cache any
      # flags for the directory of the file requested, but we did find some
      # flags previously for this file type. Return them. Once again, this is
      # an outrageous guess, but it is better to return *some* flags, rather
      # than nothing.
      print( " - Using flags for extension: " + file_extension )
      compilation_info = last_compilation_info_ext_map[ file_extension ]
    else:
      # No cache for this directory or file extension, we really can't conjure
      # up any flags from the database, just return some absolutely generic
      # fallback flags. These probably won't work for any actual project, but we
      # try anyway.
      print( " - Using generic flags" )
      return GetDefaultFlagsForFile( file_extension )

  if file_dir not in file_directory_heuristic_map:
    # This is the first file we've seen in path file_dir. Cache the
    # compilation_info for it in case we see a file in the same dir with no
    # flags available
    file_directory_heuristic_map[ file_dir ] = compilation_info

  # Cache the last successful set of compilation info that we found. As noted
  # above, this is used when we completely fail to find any flags.
  last_compilation_info_ext_map[ file_extension ] = compilation_info

  return {
    # We pass the compiler flags from the database unmodified.
    'flags': MakeRelativePathsInFlagsAbsolute(
      compilation_info.compiler_flags_,
      compilation_info.compiler_working_dir_ ),

    # We always want to use ycmd's cache, as this significantly improves
    # performance.
    'do_cache': True
  }

# }}}
