using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Text;
using System.Windows.Forms;
using Npgsql;
using NpgsqlTypes;

namespace SimpleDBViewer
{
    public partial class MainForm : Form
    {
        public const string DbSchema = "akulova_vs";

        private readonly List<TableInfo> tableInfos = new List<TableInfo>();
        private TableInfo currentTable;
        private DataTable currentTableData;
        private DataTable currentSearchData;

        public MainForm()
        {
            bool isDesigner = LicenseManager.UsageMode == LicenseUsageMode.Designtime;

            if (!isDesigner && !UserSession.IsLoggedIn)
            {
                MessageBox.Show("Сначала выполните вход в систему.", "Доступ запрещён",
                    MessageBoxButtons.OK, MessageBoxIcon.Warning);
                Close();
                return;
            }

            InitializeComponent();

            if (isDesigner)
                return;

            BuildTableList();
            ApplyPermissions();
            LoadInitialData();
        }

        private void BuildTableList()
        {
            tableInfos.Clear();

            if (!UserSession.IsAdmin)
            {
                tableInfos.Add(new TableInfo("personal_profile", "Мои данные", "driver_id"));
                tableInfos.Add(new TableInfo("personal_exams", "Мои экзамены", "exam_id"));
                tableInfos.Add(new TableInfo("personal_license", "Моё удостоверение", "license_id"));
                return;
            }

            tableInfos.Add(new TableInfo("driving_schools", "Автошколы", "school_id"));
            tableInfos.Add(new TableInfo("instructors", "Инструкторы", "instructor_id"));
            tableInfos.Add(new TableInfo("drivers", "Водители", "driver_id"));
            tableInfos.Add(new TableInfo("exam_routes", "Маршруты экзамена", "route_id"));
            tableInfos.Add(new TableInfo("traffic_inspectors", "Инспекторы", "inspector_id"));
            tableInfos.Add(new TableInfo("traffic_officers", "Сотрудники, выдающие права", "officer_id"));
            tableInfos.Add(new TableInfo("exams", "Экзамены", "exam_id"));
            tableInfos.Add(new TableInfo("licenses", "Водительские удостоверения", "license_id"));
            tableInfos.Add(new TableInfo("app_users", "Пользователи приложения", "user_id"));
            tableInfos.Add(new TableInfo("audit_log", "Журнал операций", "log_id", true));
        }
        private void ApplyPermissions()
        {
            lblUser.Text = UserSession.CurrentUser.Login + " · " + UserSession.CurrentUser.Role;

            bool isAdmin = UserSession.IsAdmin;
            btnAdd.Enabled = isAdmin;
            btnEdit.Enabled = isAdmin;
            btnDelete.Enabled = isAdmin;

            if (!isAdmin)
            {
                lblTitle.Text = "Личный кабинет автошколы";
                lblTableCaption.Text = "Раздел:";
                tabTables.Text = "Личный кабинет";

                btnAdd.Visible = false;
                btnEdit.Visible = false;
                btnDelete.Visible = false;
                btnExportTable.Location = btnAdd.Location;

                if (tabControl.TabPages.Contains(tabSearch))
                    tabControl.TabPages.Remove(tabSearch);
            }

            UpdateActionButtons();
        }

        private void UpdateActionButtons()
        {
            bool canModify = UserSession.IsAdmin && currentTable != null && !currentTable.IsReadOnly;
            if (btnAdd != null) btnAdd.Enabled = canModify;
            if (btnEdit != null) btnEdit.Enabled = canModify;
            if (btnDelete != null) btnDelete.Enabled = canModify;
        }

        private void LoadInitialData()
        {
            cmbTables.Items.Clear();
            foreach (TableInfo table in tableInfos)
            {
                cmbTables.Items.Add(table);
            }

            if (cmbTables.Items.Count > 0)
            {
                cmbTables.SelectedIndex = 0;
            }

            if (UserSession.IsAdmin)
                LoadExamSearch();
        }

        private void CmbTables_SelectedIndexChanged(object sender, EventArgs e)
        {
            currentTable = cmbTables.SelectedItem as TableInfo;
            UpdateActionButtons();
            LoadCurrentTable();
        }

        private void LoadCurrentTable()
        {
            if (currentTable == null)
                return;

            try
            {
                using (NpgsqlConnection connection = new NpgsqlConnection(DbSettings.ConnectionString))
                {
                    connection.Open();
                    string sql = GetSelectSql(currentTable);
                    using (NpgsqlCommand command = new NpgsqlCommand(sql, connection))
                    {
                        AddCurrentTableParameters(command);
                        using (NpgsqlDataAdapter adapter = new NpgsqlDataAdapter(command))
                        {
                            currentTableData = new DataTable();
                            adapter.Fill(currentTableData);
                            dgvTables.DataSource = currentTableData;
                            ApplyGridPresentation(dgvTables);
                            lblTableStatus.Text = currentTable.DisplayName + ": записей — " + currentTableData.Rows.Count;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(AppError.ToUserMessage(ex, "Не удалось загрузить таблицу."),
                    "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private void AddCurrentTableParameters(NpgsqlCommand command)
        {
            if (currentTable == null)
                return;

            if (currentTable.Name == "personal_profile" || currentTable.Name == "personal_exams" || currentTable.Name == "personal_license")
            {
                int driverId = UserSession.CurrentUser != null && UserSession.CurrentUser.DriverId.HasValue
                    ? UserSession.CurrentUser.DriverId.Value
                    : -1;
                command.Parameters.AddWithValue("@current_driver_id", driverId);
            }
        }

        private string GetSelectSql(TableInfo table)
        {
            if (table.Name == "personal_profile")
            {
                if (GetCurrentDriverCondition() == null)
                    return "select 'Учётная запись не привязана к водителю. Обратитесь к администратору.' as message";

                return @"select d.driver_id,
                                concat_ws(' ', d.last_name, d.first_name, d.middle_name) as driver_display,
                                d.birth_date,
                                d.phone,
                                ds.school_name
                         from akulova_vs.drivers d
                         join akulova_vs.driving_schools ds on ds.school_id = d.school_id
                         where d.driver_id = @current_driver_id";
            }

            if (table.Name == "personal_exams")
            {
                if (GetCurrentDriverCondition() == null)
                    return "select 'Учётная запись не привязана к водителю. Обратитесь к администратору.' as message";

                return @"select exam_id,
                                exam_date,
                                result,
                                route_name,
                                inspector_full_name,
                                attempt_number,
                                driver_age
                         from (
                             select e.exam_id,
                                    e.exam_date,
                                    e.result,
                                    r.route_name,
                                    concat_ws(' ', ti.last_name, ti.first_name, ti.middle_name) as inspector_full_name,
                                    count(*) over (
                                        partition by e.driver_id
                                        order by e.exam_date, e.exam_id
                                        rows between unbounded preceding and current row
                                    )::integer as attempt_number,
                                    extract(year from age(e.exam_date, d.birth_date))::integer as driver_age
                             from akulova_vs.exams e
                             join akulova_vs.drivers d on d.driver_id = e.driver_id
                             join akulova_vs.exam_routes r on r.route_id = e.route_id
                             join akulova_vs.traffic_inspectors ti on ti.inspector_id = e.inspector_id
                             where e.driver_id = @current_driver_id
                         ) personal_exams
                         order by exam_date desc, exam_id desc";
            }

            if (table.Name == "personal_license")
            {
                if (GetCurrentDriverCondition() == null)
                    return "select 'Учётная запись не привязана к водителю. Обратитесь к администратору.' as message";

                return @"select l.license_id,
                                l.issue_date,
                                l.category,
                                l.license_number,
                                concat_ws(' ', o.last_name, o.first_name, o.middle_name) as officer_display
                         from akulova_vs.licenses l
                         join akulova_vs.traffic_officers o on o.officer_id = l.officer_id
                         where l.driver_id = @current_driver_id
                         order by l.issue_date desc, l.license_id desc";
            }

            if (table.Name == "driving_schools")
            {
                return @"select school_id, school_name, address, phone
                         from akulova_vs.driving_schools
                         order by school_id";
            }

            if (table.Name == "instructors")
            {
                return @"select i.instructor_id, i.school_id, ds.school_name as school_display,
                                i.last_name, i.first_name, i.middle_name, i.phone, i.license_number
                         from akulova_vs.instructors i
                         join akulova_vs.driving_schools ds on ds.school_id = i.school_id
                         order by i.instructor_id";
            }

            if (table.Name == "drivers")
            {
                return @"select d.driver_id, d.school_id, ds.school_name as school_display,
                                d.last_name, d.first_name, d.middle_name, d.birth_date, d.phone
                         from akulova_vs.drivers d
                         join akulova_vs.driving_schools ds on ds.school_id = d.school_id
                         order by d.driver_id";
            }

            if (table.Name == "exam_routes")
            {
                return @"select route_id, route_name, start_address, end_address, difficulty
                         from akulova_vs.exam_routes
                         order by route_id";
            }

            if (table.Name == "traffic_inspectors")
            {
                return @"select inspector_id, last_name, first_name, middle_name, rank_name
                         from akulova_vs.traffic_inspectors
                         order by inspector_id";
            }

            if (table.Name == "traffic_officers")
            {
                return @"select officer_id, last_name, first_name, middle_name, position_name
                         from akulova_vs.traffic_officers
                         order by officer_id";
            }

            if (table.Name == "exams")
            {
                return @"select e.exam_id,
                                e.driver_id, concat_ws(' ', d.last_name, d.first_name, d.middle_name) as driver_display,
                                e.route_id, r.route_name as route_display,
                                e.inspector_id, concat_ws(' ', ti.last_name, ti.first_name, ti.middle_name) as inspector_display,
                                e.exam_date, e.result
                         from akulova_vs.exams e
                         join akulova_vs.drivers d on d.driver_id = e.driver_id
                         join akulova_vs.exam_routes r on r.route_id = e.route_id
                         join akulova_vs.traffic_inspectors ti on ti.inspector_id = e.inspector_id
                         order by e.exam_id";
            }

            if (table.Name == "licenses")
            {
                return @"select l.license_id,
                                l.driver_id, concat_ws(' ', d.last_name, d.first_name, d.middle_name) as driver_display,
                                l.officer_id, concat_ws(' ', o.last_name, o.first_name, o.middle_name) as officer_display,
                                l.issue_date, l.category, l.license_number
                         from akulova_vs.licenses l
                         join akulova_vs.drivers d on d.driver_id = l.driver_id
                         join akulova_vs.traffic_officers o on o.officer_id = l.officer_id
                         order by l.license_id";
            }

            if (table.Name == "app_users")
            {
                return @"select u.user_id, u.login, u.role::text as role, u.is_active,
                                u.instructor_id, concat_ws(' ', i.last_name, i.first_name, i.middle_name) as instructor_display,
                                u.driver_id, concat_ws(' ', d.last_name, d.first_name, d.middle_name) as driver_display
                         from akulova_vs.app_users u
                         left join akulova_vs.instructors i on i.instructor_id = u.instructor_id
                         left join akulova_vs.drivers d on d.driver_id = u.driver_id
                         order by u.user_id";
            }

            if (table.Name == "audit_log")
            {
                return @"select log_id,
                                table_name,
                                operation,
                                record_id,
                                changed_at,
                                coalesce(nullif(changed_by, ''), 'system') as changed_by
                         from akulova_vs.audit_log
                         order by changed_at desc, log_id desc
                         limit 500";
            }

            return "select * from " + FullName(table.Name) + " order by " + Quote(table.PrimaryKey);
        }

        private string GetCurrentDriverCondition()
        {
            if (UserSession.CurrentUser == null || !UserSession.CurrentUser.DriverId.HasValue)
                return null;

            return UserSession.CurrentUser.DriverId.Value.ToString();
        }

        private void ApplyGridPresentation(DataGridView grid)
        {
            if (grid == null)
                return;

            foreach (DataGridViewColumn column in grid.Columns)
            {
                string name = column.Name;
                column.Visible = !IsTechnicalColumn(name);
                column.HeaderText = GetColumnCaption(name);

                if (name.EndsWith("_date", StringComparison.OrdinalIgnoreCase) || name == "birth_date" || name == "exam_date" || name == "issue_date")
                    column.DefaultCellStyle.Format = "dd.MM.yyyy";
            }

            grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
            grid.AutoResizeColumns();
        }

        private bool IsTechnicalColumn(string columnName)
        {
            if (string.IsNullOrEmpty(columnName))
                return false;

            if (columnName == "password_hash")
                return true;

            if (columnName == "record_id")
                return false;

            if (columnName.EndsWith("_id", StringComparison.OrdinalIgnoreCase))
                return true;

            return false;
        }

        private string GetColumnCaption(string columnName)
        {
            Dictionary<string, string> captions = new Dictionary<string, string>
            {
                { "school_name", "Название автошколы" },
                { "address", "Адрес" },
                { "phone", "Телефон" },
                { "school_display", "Автошкола" },
                { "last_name", "Фамилия" },
                { "first_name", "Имя" },
                { "middle_name", "Отчество" },
                { "license_number", "Номер удостоверения" },
                { "birth_date", "Дата рождения" },
                { "route_name", "Название маршрута" },
                { "route_display", "Маршрут" },
                { "start_address", "Начальный адрес" },
                { "end_address", "Конечный адрес" },
                { "difficulty", "Сложность" },
                { "rank_name", "Звание" },
                { "position_name", "Должность" },
                { "driver_display", "Водитель" },
                { "message", "Сообщение" },
                { "inspector_display", "Инспектор" },
                { "officer_display", "Сотрудник" },
                { "exam_date", "Дата экзамена" },
                { "result", "Результат" },
                { "issue_date", "Дата выдачи" },
                { "category", "Категория" },
                { "login", "Логин" },
                { "role", "Роль" },
                { "is_active", "Активен" },
                { "instructor_display", "Инструктор" },
                { "driver_full_name", "Водитель" },
                { "inspector_full_name", "Инспектор" },
                { "attempt_number", "Попытка №" },
                { "driver_age", "Возраст водителя" },
                { "table_name", "Таблица" },
                { "operation", "Операция" },
                { "record_id", "ID записи" },
                { "changed_at", "Дата и время" },
                { "changed_by", "Пользователь" }
            };

            return captions.ContainsKey(columnName) ? captions[columnName] : columnName;
        }

        private void BtnRefresh_Click(object sender, EventArgs e)
        {
            LoadCurrentTable();
        }

        private void BtnAdd_Click(object sender, EventArgs e)
        {
            if (!UserSession.IsAdmin)
            {
                ShowReadonlyMessage();
                return;
            }

            if (currentTable == null)
                return;

            if (currentTable.IsReadOnly)
            {
                MessageBox.Show("Этот раздел предназначен только для просмотра.", "Информация",
                    MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }

            using (RecordEditForm form = new RecordEditForm(DbSettings.ConnectionString, currentTable, null))
            {
                if (form.ShowDialog(this) == DialogResult.OK)
                {
                    LoadCurrentTable();
                    LoadExamSearch();
                }
            }
        }

        private void BtnEdit_Click(object sender, EventArgs e)
        {
            if (!UserSession.IsAdmin)
            {
                ShowReadonlyMessage();
                return;
            }

            if (currentTable != null && currentTable.IsReadOnly)
            {
                MessageBox.Show("Этот раздел предназначен только для просмотра.", "Информация",
                    MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }

            if (currentTable == null || dgvTables.SelectedRows.Count == 0)
            {
                MessageBox.Show("Выберите запись для изменения.", "Информация",
                    MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }

            DataRowView rowView = dgvTables.SelectedRows[0].DataBoundItem as DataRowView;
            if (rowView == null)
                return;

            using (RecordEditForm form = new RecordEditForm(DbSettings.ConnectionString, currentTable, rowView.Row))
            {
                if (form.ShowDialog(this) == DialogResult.OK)
                {
                    LoadCurrentTable();
                    LoadExamSearch();
                }
            }
        }

        private void BtnDelete_Click(object sender, EventArgs e)
        {
            if (!UserSession.IsAdmin)
            {
                ShowReadonlyMessage();
                return;
            }

            if (currentTable != null && currentTable.IsReadOnly)
            {
                MessageBox.Show("Этот раздел предназначен только для просмотра.", "Информация",
                    MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }

            if (currentTable == null || dgvTables.SelectedRows.Count == 0)
            {
                MessageBox.Show("Выберите запись для удаления.", "Информация",
                    MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }

            DataRowView rowView = dgvTables.SelectedRows[0].DataBoundItem as DataRowView;
            if (rowView == null)
                return;

            object pkValue = rowView.Row[currentTable.PrimaryKey];

            if (currentTable.Name == "app_users" && Convert.ToInt32(pkValue) == UserSession.CurrentUser.UserId)
            {
                MessageBox.Show("Нельзя удалить текущую учётную запись, под которой выполнен вход.",
                    "Удаление запрещено", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }

            DialogResult result = MessageBox.Show("Удалить выбранную запись?",
                "Подтверждение", MessageBoxButtons.YesNo, MessageBoxIcon.Question);

            if (result != DialogResult.Yes)
                return;

            try
            {
                using (NpgsqlConnection connection = new NpgsqlConnection(DbSettings.ConnectionString))
                {
                    connection.Open();
                    DbAppContext.Apply(connection);
                    string sql = "delete from " + FullName(currentTable.Name) + " where " + Quote(currentTable.PrimaryKey) + " = @id";
                    using (NpgsqlCommand command = new NpgsqlCommand(sql, connection))
                    {
                        command.Parameters.AddWithValue("@id", pkValue);
                        command.ExecuteNonQuery();
                        DbAppContext.MarkLatestAuditLogin(connection, currentTable.Name, pkValue);
                    }
                }

                LoadCurrentTable();
                LoadExamSearch();
            }
            catch (Exception ex)
            {
                MessageBox.Show(AppError.ToUserMessage(ex, "Не удалось удалить запись."),
                    "Ошибка удаления", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private void ShowReadonlyMessage()
        {
            MessageBox.Show("Обычный пользователь работает только с личным кабинетом. Изменение данных доступно только роли admin.",
                "Недостаточно прав", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

        private void BtnExportTable_Click(object sender, EventArgs e)
        {
            ExportTable(dgvTables, currentTable == null ? "table" : currentTable.Name);
        }

        private void BtnSearch_Click(object sender, EventArgs e)
        {
            LoadExamSearch();
        }

        private void BtnReset_Click(object sender, EventArgs e)
        {
            txtDriverFilter.Clear();
            txtSchoolFilter.Clear();
            cmbResultFilter.SelectedIndex = 0;
            dtFrom.Checked = false;
            dtTo.Checked = false;
            LoadExamSearch();
        }

        private void BtnExportSearch_Click(object sender, EventArgs e)
        {
            ExportTable(dgvSearch, "exam_search");
        }

        private void LoadExamSearch()
        {
            try
            {
                using (NpgsqlConnection connection = new NpgsqlConnection(DbSettings.ConnectionString))
                {
                    connection.Open();

                    const string sql = @"
                        select exam_id, exam_date, result, driver_full_name, school_name, route_name, inspector_full_name, attempt_number, driver_age
                        from akulova_vs.v_exam_search
                        where (cast(@driver as text) = '' or driver_full_name ilike '%' || cast(@driver as text) || '%')
                          and (cast(@school as text) = '' or school_name ilike '%' || cast(@school as text) || '%')
                          and (cast(@result as text) = '' or result = cast(@result as text))
                          and (cast(@date_from as date) is null or exam_date >= cast(@date_from as date))
                          and (cast(@date_to as date) is null or exam_date <= cast(@date_to as date))
                        order by exam_date desc, exam_id desc;";

                    using (NpgsqlCommand command = new NpgsqlCommand(sql, connection))
                    {
                        command.Parameters.AddWithValue("@driver", txtDriverFilter == null ? "" : txtDriverFilter.Text.Trim());
                        command.Parameters.AddWithValue("@school", txtSchoolFilter == null ? "" : txtSchoolFilter.Text.Trim());
                        string selectedResult = cmbResultFilter == null || cmbResultFilter.SelectedIndex <= 0
                            ? ""
                            : cmbResultFilter.SelectedItem.ToString();
                        command.Parameters.AddWithValue("@result", selectedResult);

                        Npgsql.NpgsqlParameter dateFromParameter = command.Parameters.Add("@date_from", NpgsqlTypes.NpgsqlDbType.Date);
                        dateFromParameter.Value = dtFrom != null && dtFrom.Checked ? (object)dtFrom.Value.Date : DBNull.Value;

                        Npgsql.NpgsqlParameter dateToParameter = command.Parameters.Add("@date_to", NpgsqlTypes.NpgsqlDbType.Date);
                        dateToParameter.Value = dtTo != null && dtTo.Checked ? (object)dtTo.Value.Date : DBNull.Value;

                        using (NpgsqlDataAdapter adapter = new NpgsqlDataAdapter(command))
                        {
                            currentSearchData = new DataTable();
                            adapter.Fill(currentSearchData);
                            if (dgvSearch != null)
                            {
                                dgvSearch.DataSource = currentSearchData;
                                ApplyGridPresentation(dgvSearch);
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                if (dgvSearch != null)
                {
                    MessageBox.Show(AppError.ToUserMessage(ex, "Не удалось выполнить поиск по экзаменам."),
                        "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }

        private void ExportTable(DataGridView grid, string filePrefix)
        {
            if (grid == null || grid.DataSource == null || grid.Rows.Count == 0)
            {
                MessageBox.Show("Нет данных для экспорта.", "Информация",
                    MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }

            using (SaveFileDialog dialog = new SaveFileDialog())
            {
                dialog.Filter = "CSV файл (*.csv)|*.csv";
                dialog.FileName = filePrefix + "_" + DateTime.Now.ToString("yyyyMMdd_HHmm") + ".csv";

                if (dialog.ShowDialog(this) != DialogResult.OK)
                    return;

                try
                {
                    WriteCsv(grid, dialog.FileName);
                    MessageBox.Show("CSV-файл сохранён:\n" + dialog.FileName,
                        "Экспорт выполнен", MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
                catch (Exception ex)
                {
                    MessageBox.Show("Не удалось сохранить CSV.\n\n" + ex.Message,
                        "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }

        private void WriteCsv(DataGridView grid, string path)
        {
            List<DataGridViewColumn> visibleColumns = new List<DataGridViewColumn>();
            foreach (DataGridViewColumn column in grid.Columns)
            {
                if (column.Visible)
                    visibleColumns.Add(column);
            }

            using (StreamWriter writer = new StreamWriter(path, false, new UTF8Encoding(true)))
            {
                for (int i = 0; i < visibleColumns.Count; i++)
                {
                    if (i > 0) writer.Write(';');
                    writer.Write(EscapeCsv(visibleColumns[i].HeaderText));
                }
                writer.WriteLine();

                foreach (DataGridViewRow row in grid.Rows)
                {
                    if (row.IsNewRow)
                        continue;

                    for (int i = 0; i < visibleColumns.Count; i++)
                    {
                        if (i > 0) writer.Write(';');
                        object value = row.Cells[visibleColumns[i].Name].Value;
                        writer.Write(EscapeCsv(value == null || value == DBNull.Value ? "" : value.ToString()));
                    }
                    writer.WriteLine();
                }
            }
        }

        private string EscapeCsv(string value)
        {
            if (value == null) return "";
            bool needQuotes = value.Contains(";") || value.Contains("\"") || value.Contains("\n") || value.Contains("\r");
            value = value.Replace("\"", "\"\"");
            return needQuotes ? "\"" + value + "\"" : value;
        }

        private void BtnLogout_Click(object sender, EventArgs e)
        {
            UserSession.Logout();
            Close();
        }

        public static string FullName(string identifier)
        {
            return Quote(DbSchema) + "." + Quote(identifier);
        }

        public static string Quote(string identifier)
        {
            return "\"" + identifier.Replace("\"", "\"\"") + "\"";
        }
    }

    public class TableInfo
    {
        public string Name { get; private set; }
        public string DisplayName { get; private set; }
        public string PrimaryKey { get; private set; }
        public bool IsReadOnly { get; private set; }

        public TableInfo(string name, string displayName, string primaryKey)
            : this(name, displayName, primaryKey, false)
        {
        }

        public TableInfo(string name, string displayName, string primaryKey, bool isReadOnly)
        {
            Name = name;
            DisplayName = displayName;
            PrimaryKey = primaryKey;
            IsReadOnly = isReadOnly;
        }

        public override string ToString()
        {
            return DisplayName;
        }
    }

    public class ColumnInfo
    {
        public string Name { get; set; }
        public string DataType { get; set; }
        public string UdtName { get; set; }
        public bool IsNullable { get; set; }
        public bool IsIdentity { get; set; }
        public bool IsPrimaryKey { get; set; }
    }

    public class ComboItem
    {
        public int? Id { get; private set; }
        public string Text { get; private set; }

        public ComboItem(int? id, string text)
        {
            Id = id;
            Text = text;
        }

        public override string ToString()
        {
            return Text;
        }
    }

    public class FieldBinding
    {
        public ColumnInfo Column { get; set; }
        public Control Control { get; set; }
        public bool IsPasswordField { get; set; }
    }
}
