# Copyright (C) 2009-2020 the sqlparse authors and contributors
# <see AUTHORS file>
#
# This module is part of python-sqlparse and is released under
# the BSD License: https://opensource.org/licenses/BSD-3-Clause

"""Module that contains the command line app.

Why does this file exist, and why not put this in __main__?
  You might be tempted to import things from __main__ later, but that will
  cause problems: the code will get executed twice:
  - When you run `python -m sqlparse` python will execute
    ``__main__.py`` as a script. That means there won't be any
    ``sqlparse.__main__`` in ``sys.modules``.
  - When you import __main__ it will get executed again (as a module) because
    there's no ``sqlparse.__main__`` in ``sys.modules``.
  Also see (1) from http://click.pocoo.org/5/setuptools/#setuptools-integration
"""

import argparse
import sys
from io import TextIOWrapper

import sqlparse
from sqlparse.exceptions import SQLParseError


# TODO: Add CLI Tests
# TODO: Simplify formatter by using argparse `type` arguments
def create_parser():
    _CASE_CHOICES = ['upper', 'lower', 'capitalize']

    parser = argparse.ArgumentParser(
        prog='sqlformat',
        description='Format FILE according to OPTIONS. Use "-" as FILE '
                    'to read from stdin.',
        usage='%(prog)s [OPTIONS] FILE [FILE ...]',
    )

    parser.add_argument(
        'filename',
        nargs='+',
        help='file(s) to format (use "-" for stdin)')

    parser.add_argument(
        '-o', '--outfile',
        dest='outfile',
        metavar='FILE',
        help='write output to FILE (defaults to stdout)')

    parser.add_argument(
        '--in-place',
        dest='inplace',
        action='store_true',
        default=False,
        help='format files in-place (overwrite existing files)')

    parser.add_argument(
        '--version',
        action='version',
        version=sqlparse.__version__)

    group = parser.add_argument_group('Formatting Options')

    group.add_argument(
        '-k', '--keywords',
        metavar='CHOICE',
        dest='keyword_case',
        choices=_CASE_CHOICES,
        help='change case of keywords, CHOICE is one of {}'.format(
            ', '.join(f'"{x}"' for x in _CASE_CHOICES)))

    group.add_argument(
        '-i', '--identifiers',
        metavar='CHOICE',
        dest='identifier_case',
        choices=_CASE_CHOICES,
        help='change case of identifiers, CHOICE is one of {}'.format(
            ', '.join(f'"{x}"' for x in _CASE_CHOICES)))

    group.add_argument(
        '-l', '--language',
        metavar='LANG',
        dest='output_format',
        choices=['python', 'php'],
        help='output a snippet in programming language LANG, '
             'choices are "python", "php"')

    group.add_argument(
        '--strip-comments',
        dest='strip_comments',
        action='store_true',
        default=False,
        help='remove comments')

    group.add_argument(
        '-r', '--reindent',
        dest='reindent',
        action='store_true',
        default=False,
        help='reindent statements')

    group.add_argument(
        '--indent_width',
        dest='indent_width',
        default=2,
        type=int,
        help='indentation width (defaults to 2 spaces)')

    group.add_argument(
        '--indent_after_first',
        dest='indent_after_first',
        action='store_true',
        default=False,
        help='indent after first line of statement (e.g. SELECT)')

    group.add_argument(
        '--indent_columns',
        dest='indent_columns',
        action='store_true',
        default=False,
        help='indent all columns by indent_width instead of keyword length')

    group.add_argument(
        '-a', '--reindent_aligned',
        action='store_true',
        default=False,
        help='reindent statements to aligned format')

    group.add_argument(
        '-s', '--use_space_around_operators',
        action='store_true',
        default=False,
        help='place spaces around mathematical operators')

    group.add_argument(
        '--wrap_after',
        dest='wrap_after',
        default=0,
        type=int,
        help='Column after which lists should be wrapped')

    group.add_argument(
        '--comma_first',
        dest='comma_first',
        default=False,
        type=bool,
        help='Insert linebreak before comma (default False)')

    group.add_argument(
        '--compact',
        dest='compact',
        default=False,
        type=bool,
        help='Try to produce more compact output (default False)')

    group.add_argument(
        '--encoding',
        dest='encoding',
        default='utf-8',
        help='Specify the input encoding (default utf-8)')

    return parser


def _error(msg):
    """Print msg and optionally exit with return code exit_."""
    sys.stderr.write(f'[ERROR] {msg}\n')
    return 1


def _process_file(filename, args):
    """Process a single file with the given formatting options.

    Returns 0 on success, 1 on error.
    """
    # Check for incompatible option combinations first
    if filename == '-' and args.inplace:
        return _error('Cannot use --in-place with stdin')

    # Read input
    if filename == '-':  # read from stdin
        wrapper = TextIOWrapper(sys.stdin.buffer, encoding=args.encoding)
        try:
            data = wrapper.read()
        finally:
            wrapper.detach()
    else:
        try:
            with open(filename, encoding=args.encoding) as f:
                data = ''.join(f.readlines())
        except OSError as e:
            return _error(f'Failed to read {filename}: {e}')

    # Determine output destination
    close_stream = False
    if args.inplace:
        try:
            stream = open(filename, 'w', encoding=args.encoding)
            close_stream = True
        except OSError as e:
            return _error(f'Failed to open {filename}: {e}')
    elif args.outfile:
        try:
            stream = open(args.outfile, 'w', encoding=args.encoding)
            close_stream = True
        except OSError as e:
            return _error(f'Failed to open {args.outfile}: {e}')
    else:
        stream = sys.stdout

    # Format the SQL
    formatter_opts = vars(args)
    try:
        formatter_opts = sqlparse.formatter.validate_options(formatter_opts)
    except SQLParseError as e:
        return _error(f'Invalid options: {e}')

    s = sqlparse.format(data, **formatter_opts)
    stream.write(s)
    stream.flush()
    if close_stream:
        stream.close()
    return 0


def main(args=None):
    parser = create_parser()
    args = parser.parse_args(args)

    # Validate argument combinations
    if len(args.filename) > 1:
        if args.outfile:
            return _error('Cannot use -o/--outfile with multiple files')
        if not args.inplace:
            return _error('Multiple files require --in-place flag')

    # Process all files
    exit_code = 0
    for filename in args.filename:
        result = _process_file(filename, args)
        if result != 0:
            exit_code = result
            # Continue processing remaining files even if one fails

    return exit_code
