// Copyright © 2016, 2017, Oracle and/or its affiliates. All rights reserved.
//
// MySQL for Visual Studio is licensed under the terms of the GPLv2
// <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most
// MySQL Connectors. There are special exceptions to the terms and
// conditions of the GPLv2 as it is applied to this software, see the
// FLOSS License Exception
// <http://www.mysql.com/about/legal/licensing/foss-exception.html>.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published
// by the Free Software Foundation; version 2 of the License.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
// for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace ConsoleTables.Core
{
  public class ConsoleTable
  {
    public IList<object> Columns { get; protected set; }
    public IList<object[]> Rows { get; protected set; }

    public ConsoleTable(params string[] columns)
    {
      Columns = new List<object>(columns);
      Rows = new List<object[]>();
    }

    public ConsoleTable AddColumn(IEnumerable<string> names)
    {
      foreach (var name in names)
        Columns.Add(name);
      return this;
    }

    public ConsoleTable AddRow(params object[] values)
    {
      if (values == null)
        throw new ArgumentNullException("values");

      if (!Columns.Any())
        throw new Exception("Please set the columns first");

      if (Columns.Count != values.Length)
        throw new Exception(string.Format("The number columns in the row ({0}) does not match the values ({1})", Columns.Count, values.Length));

      Rows.Add(values);
      return this;
    }

    public static ConsoleTable From<T>(IEnumerable<T> values)
    {
      var table = new ConsoleTable();

      var columns = typeof(T).GetProperties().Select(x => x.Name).ToArray();
      table.AddColumn(columns);

      foreach (var propertyValues in values.Select(value => columns.Select(column => typeof(T).GetProperty(column).GetValue(value, null))))
        table.AddRow(propertyValues.ToArray());

      return table;
    }

    public override string ToString()
    {
      var builder = new StringBuilder();

      // find the longest column by searching each row
      var columnLengths = ColumnLengths();

      // create the string format with padding
      var format = Enumerable.Range(0, Columns.Count)
          .Select(i => " | {" + i + ", -" + columnLengths[i] + " }")
          .Aggregate((s, a) => s + a) + " |";

      var results = new List<string>();

      // find the longest formatted line
      var maxRowLength = Math.Max(0, Rows.Any() ? Rows.Max(row => string.Format(format, row).Length) : 0);
      var columnHeaders = string.Format(format, Columns.ToArray());

      // longest line is greater of formatted columnHeader and longest row
      var longestLine = Math.Max(maxRowLength, columnHeaders.Length);

      // add each row
      Array.ForEach(Rows.Select(row => string.Format(format, row)).ToArray(), results.Add);

      // create the divider
      var divider = " " + string.Join("", Enumerable.Repeat("-", longestLine - 1)) + " ";

      builder.AppendLine(divider);
      builder.AppendLine(columnHeaders);

      foreach (var row in results)
      {
        builder.AppendLine(divider);
        builder.AppendLine(row);
      }

      builder.AppendLine(divider);
      builder.AppendLine("");
      builder.AppendFormat(" Count: {0}", Rows.Count);

      return builder.ToString();
    }

    public string ToMarkDownString()
    {
      var builder = new StringBuilder();

      // find the longest column by searching each row
      var columnLengths = ColumnLengths();

      // create the string format with padding
      var format = Format(columnLengths);

      var results = new List<string>();

      // find the longest formatted line
      var columnHeaders = string.Format(format, Columns.ToArray());

      // add each row
      Array.ForEach(Rows.Select(row => string.Format(format, row)).ToArray(), results.Add);

      // create the divider
      var divider = Regex.Replace(columnHeaders, @"[^|]", "-");

      builder.AppendLine(columnHeaders);
      builder.AppendLine(divider);
      results.ForEach(row => builder.AppendLine(row));

      return builder.ToString();
    }

    public string ToStringAlternative()
    {
      var builder = new StringBuilder();

      // find the longest column by searching each row
      var columnLengths = ColumnLengths();

      // create the string format with padding
      var format = Format(columnLengths);

      var results = new List<string>();

      // find the longest formatted line
      var columnHeaders = string.Format(format, Columns.ToArray());

      // add each row
      Array.ForEach(Rows.Select(row => string.Format(format, row)).ToArray(), results.Add);

      // create the divider
      var divider = Regex.Replace(columnHeaders, @"[^|]", "-");
      var dividerPlus = divider.Replace("|", "+");

      builder.AppendLine(dividerPlus);
      builder.AppendLine(columnHeaders);

      foreach (var row in results)
      {
        builder.AppendLine(dividerPlus);
        builder.AppendLine(row);
      }
      builder.AppendLine(dividerPlus);

      return builder.ToString();
    }

    private string Format(List<int> columnLengths)
    {
      var format = (Enumerable.Range(0, Columns.Count)
          .Select(i => " | {" + i + ", -" + columnLengths[i] + " }")
          .Aggregate((s, a) => s + a) + " |").Trim();
      return format;
    }

    private List<int> ColumnLengths()
    {
      var columnLengths = Columns
          .Select((t, i) => Rows.Select(x => x[i])
              .Union(Columns)
              .Where(x => x != null)
              .Select(x => x.ToString().Length).Max())
          .ToList();
      return columnLengths;
    }

    public void Write(Format format = Core.Format.Default)
    {
      switch (format)
      {
        case Core.Format.Default:
          Console.WriteLine(ToString());
          break;
        case Core.Format.MarkDown:
          Console.WriteLine(ToMarkDownString());
          break;
        case Core.Format.Alternative:
          Console.WriteLine(ToStringAlternative());
          break;
        default:
          throw new ArgumentOutOfRangeException("format", format, null);
      }
    }
  }

  public enum Format
  {
    Default = 0,
    MarkDown = 1,
    Alternative = 2
  }
}
