function! python_imports#filename2module(filename)
  " Figure out the dotted module name of the given filename

  " Look at the file name of the module that contains this tag.  Find the
  " nearest parent directory that does not have __init__.py.  Assume it is
  " directly included in PYTHONPATH.
  let pkg = fnamemodify(a:filename, ":p")
  let root = fnamemodify(pkg, ":h")

  " normalize paths
  let pythonPathsNorm = []
  for path in g:pythonPaths
    let path_without_slash = substitute(expand(path), "/$", "", "")
    call add(pythonPathsNorm, path_without_slash)
  endfor

  let found_dir = ""
  let found_path = ""
  while 1
    if index(pythonPathsNorm, root) != -1
      let found_path = root
      break
    endif
    if found_dir == "" && !filereadable(root . "/__init__.py")
      let found_dir = root
      " note: can't break here!  PEP 420 implicit namespace packages don't have __init__.py,
      " so we might find the actual package root in a parent directory beyond this one, via pythonPathsNorm
    endif
    let newroot = fnamemodify(root, ":h")
    if newroot == root
      break
    endif
    let root = newroot
  endwhile
  if found_path != ""
    let root = found_path
  else
    let root = found_dir
  endif

  let pkg = strpart(pkg, strlen(root))
  " Convert the relative path into a Python dotted module name
  let pkg = substitute(pkg, "\\", "/", "g") " Handle Windows paths
  let pkg = substitute(pkg, "[.]py$", "", "")
  let pkg = substitute(pkg, ".__init__$", "", "")
  let pkg = substitute(pkg, "^/", "", "")
  let pkg = substitute(pkg, "^site-packages/", "", "")
  let pkg = substitute(pkg, "/", ".", "g")
  " Get rid of the last module name if it starts with an underscore, e.g.
  " zope.schema._builtinfields -> zope.schema
  let pkg = substitute(pkg, "[.]_[a-zA-Z0-9_]*$", "", "")
  return pkg
endfunction

function! python_imports#filename2package(filename)
  let module = python_imports#filename2module(a:filename)
  let pkg = python_imports#package_of(module)
  return pkg
endfunction

function! python_imports#package_of(module)
  let pkg = substitute(a:module, '[.]\=[^.]\+$', '', '')
  return pkg
endfunction

function! python_imports#is_stdlib_module(name)
  " Does a:name refer to a standard library module?

  if has_key(g:pythonBuiltinModules, a:name)
    return 1
  elseif g:pythonStdlibPath == ""
    return 0
  elseif filereadable(g:pythonStdlibPath . "/" . a:name . ".py")
    return 1
  elseif filereadable(g:pythonStdlibPath . "/" . a:name . "/__init__.py")
    return 1
  elseif filereadable(g:pythonStdlibPath . "/lib-dynload/" . a:name . ".so")
    return 1
  elseif filereadable(g:pythonStdlibPath . "/lib-dynload/" . a:name . g:pythonExtModuleSuffix)
    return 1
  else
    return 0
  endif
endfunction

function! python_imports#maybe_reload_config()
  if has('python') || has('python3')
    " XXX: wasteful -- I should check if the file's timestamp has changed
    " instead of parsing it every time
    pyx import python_imports
    pyx python_imports.parse_python_imports_cfg()
  endif
endfunction

function! python_imports#find_place_for_import(pkg, name)
  " Find the appropriate place to insert a "from pkg import name" line.
  " Moves the actual cursor in the actual Vim buffer.

  " Go to the top (use 'normal gg' because I want to set the ' mark)
  normal gg
  if getline(line(".")) =~ '^"""'
    keepjumps silent! /^"""/                 " Skip docstring, if it exists
  else
    keepjumps silent! /^"""/;/^"""/          " Skip docstring, if it exists
  endif
  keepjumps silent! /^import\|^from.*import/ " Find the first import statement
  nohlsearch
  if a:pkg == '__future__'
    return
  endif
  " Find the first empty line after that.  NOTE: DO NOT put any comments
  " on the line that says `normal`, or you'll get 24 extra spaces here
  if getline(line(".")) =~ 'import\|"""\|^#'
    keepjumps normal }
  endif
  " Try to find an existing import from the same module, and move to
  " the last one of these
  let pkg = a:pkg
  while pkg != ""
    let stmt = "from ".pkg." "      " look for an exact match first
    if search('^' . stmt, 'cnw')
        exec "keepjumps silent! /^".stmt."/;/^\\(".stmt."\\)\\@!/"
        nohlsearch
        break
    endif
    let stmt = "from ".pkg."."      " try siblings or subpackages
    if search('^' . stmt, 'cnw')
        exec "keepjumps silent! /^".stmt."/;/^\\(".stmt."\\)\\@!/"
        nohlsearch
        break
    endif
    " If not found, look for imports coming from containing packages
    if pkg =~ '[.]'
      let pkg = substitute(pkg, '[.][^.]*$', '', '')
    else
      break
    endif
  endwhile
endf

if v:version >= 801 || v:version == 800 && has("patch-499")
  function! s:taglist(tag, filename)
    return taglist(a:tag, a:filename)
  endf
else
  function! s:taglist(tag, filename)
    return taglist(a:tag)
  endf
endif

function! python_imports#import_name(name, here, stay)
  " Add an import statement for 'name'.  If 'here' is true, adds the statement
  " on the line above the cursor, if 'here' is false, adds the line to the top
  " of the current file.  If 'stay' is true, keeps cursor position, otherwise
  " jumps to the line containing the newly added import statement.

  call python_imports#maybe_reload_config()

  " If name is empty, pick up the word under cursor
  if a:name == ""
    let name = expand("<cword>")
  else
    let name = a:name
  endif

  let alias = l:name
  let l:name = get(g:pythonImportAliases, alias, alias)

  " Look for hardcoded names
  if has_key(g:pythonImports, l:name)
    let pkg = g:pythonImports[l:name]
  elseif python_imports#is_stdlib_module(l:name)
    let pkg = ''
  else
    " Let's see if we have one tag, or multiple tags (in which case we'll
    " let the user decide)
    let tag_rx = "^\\C" . l:name . "\\([.]py\\)\\=$"
    let found = s:taglist(tag_rx, expand("%"))
    if found == []
      " Give up and bail out
      echohl Error | echomsg "Tag not found:" l:name | echohl None
      return
    elseif len(found) == 1
      " Only one name found, we can skip the selection menu and the
      " whole costly procedure of opening split windows.
      let pkg = python_imports#filename2module(found[0].filename)
      if found[0].kind == 'F'
        " importing an entire module
        let pkg = python_imports#package_of(pkg)
      endif
    else
      " Try to jump to the tag in a new window
      let v:errmsg = ""
      let l:errmsg = ""
      let l:oldfile = expand('%')
      let l:oldswb = &switchbuf
      set switchbuf=split
      let l:oldwinnr = winnr()
      try
        exec "stjump /" . tag_rx
      catch
        let l:errmsg = v:errmsg
      finally
        let &switchbuf = l:oldswb
      endtry
      if l:errmsg != ""
        " Something bad happened (maybe the other file is opened in a
        " different vim instance and there's a swap file)
        if l:oldfile != expand('%')
          close
          exec l:oldwinnr "wincmd w"
        endif
        return
      endif
      if l:oldfile == expand('%')
        " Either the user aborted the tag jump, or the tag exists in
        " the same file, and therefore import is pointless
        return
      endif
      " Look at the file name of the module that contains this tag.  Find the
      " nearest parent directory that does not have __init__.py.  Assume it is
      " directly included in PYTHONPATH.
      let pkg = python_imports#filename2module(expand("%"))
      let nonfile_tags = found->filter({idx, val -> val.kind != 'F'})
      if expand("%:t") == l:name . ".py" && nonfile_tags == []
        let pkg = python_imports#package_of(pkg)
      endif
      " Close the window containing the tag
      close
      " Return to the right window
      exec l:oldwinnr "wincmd w"
    endif
  endif

  if pkg == ""
    let line_to_insert = 'import ' . l:name
  elseif pkg == "__future__" && l:name == "print"
    let line_to_insert = 'from __future__ import print_function'
  else
    let line_to_insert = 'from ' . pkg . ' import ' . l:name
  endif
  if l:alias != l:name
    let line_to_insert .= ' as ' . l:alias
  endif

  " Find the place for adding the import statement
  if !a:here
    if search('^' . line_to_insert . '$', 'bcnw')
      " import already exists
      redraw
      echomsg l:name . " is already imported"
      return
    endif
    call python_imports#find_place_for_import(pkg, l:name)
  endif
  " Find out the indentation of the current line
  let indent = matchstr(getline("."), "^[ \t]*\\%(>>> \\)\\=")
  " Check if we're using parenthesized imports already
  let prev_line = getline(line(".")-1)
  let stopline = 0
  if indent != "" && prev_line  == 'from ' . pkg . ' import ('
    if l:alias != l:name
      let line_to_insert = l:name . ' as ' . l:alias . ','
    else
      let line_to_insert = l:name . ','
    endif
    let stopline = search(")", "nW")
  elseif indent != "" && prev_line =~ '^from .* import ('
    silent! /)/+1
    nohlsearch
    if line(".") == line("$") && getline(line(".")-1) !~ ')'
      put =''
    endif
    let indent = ""
  endif
  let line_to_insert = indent . line_to_insert
  " Double check with indent / parenthesized form
  if !a:here && search('^' . line_to_insert . '$', 'cnw', stopline)
    " import already exists
    redraw
    echomsg l:name . " is already imported"
    return
  endif
  " Add the import statement
  put! =line_to_insert
  " Adjust import location with isort if possible
  if !a:here && g:pythonImportsUseAleFix && exists(":ALEFix") == 2
    ALEFix isort
  endif
  " Jump back if possible
  if a:stay
    normal ``
  endif
  " Refresh ALE because otherwise it gets all confused for a bit
  if exists(":ALELint") == 2
    if exists(":ALEResetBuffer") == 2
      ALEResetBuffer
    endif
    ALELint
  endif
endf