import { Router } from "express";
import type { ParamsDictionary } from "express-serve-static-core";
import type { RowDataPacket, ResultSetHeader, ExecuteValues } from "mysql2";
import type { Server } from "socket.io";
import db from "../db.js";
import { broadcastToCampaign } from "../socket.js";
import { parseJson } from "../utils/parseJson.js";
import { requireAuth } from "../auth/middleware.js";

type CampaignParams = ParamsDictionary & { campaignId: string };

const router = Router({ mergeParams: true });

const DEFAULT_STATS = ["STR", "DEX", "CON", "INT", "WIS", "CHA"];

function generateCharacterColor(): string {
  const hue = Math.floor(Math.random() * 360);
  return `hsl(${hue}, 60%, 65%)`;
}

function parseGear(rows: RowDataPacket[]) {
  return rows.map((r) => ({
    ...r,
    properties: parseJson(r.properties),
    effects: parseJson(r.effects),
  }));
}

function parseTalents(rows: RowDataPacket[]) {
  return rows.map((r) => ({ ...r, effect: parseJson(r.effect) }));
}

async function canModifyCharacter(characterId: string, userId: number): Promise<boolean> {
  // DMs can modify any character in their campaign
  const [dmCheck] = await db.execute<RowDataPacket[]>(
    `SELECT cm.role FROM campaign_members cm
     JOIN characters c ON c.campaign_id = cm.campaign_id
     WHERE c.id = ? AND cm.user_id = ? AND cm.role = 'dm'`,
    [characterId, userId]
  );
  if (dmCheck.length > 0) return true;

  // Players can only modify their own characters
  const [ownerCheck] = await db.execute<RowDataPacket[]>(
    "SELECT id FROM characters WHERE id = ? AND user_id = ?",
    [characterId, userId]
  );
  return ownerCheck.length > 0;
}

async function enrichCharacters(characters: RowDataPacket[]) {
  return Promise.all(
    characters.map(async (char) => {
      const [stats] = await db.execute<RowDataPacket[]>(
        "SELECT stat_name, value FROM character_stats WHERE character_id = ?",
        [char.id]
      );
      const [gear] = await db.execute<RowDataPacket[]>(
        "SELECT * FROM character_gear WHERE character_id = ?",
        [char.id]
      );
      const [talents] = await db.execute<RowDataPacket[]>(
        "SELECT * FROM character_talents WHERE character_id = ?",
        [char.id]
      );
      return {
        ...char,
        overrides: parseJson(char.overrides),
        stats,
        gear: parseGear(gear),
        talents: parseTalents(talents),
      };
    })
  );
}

// GET /api/campaigns/:campaignId/characters
router.get<CampaignParams>("/", async (req, res) => {
  try {
    const { campaignId } = req.params;
    const [characters] = await db.execute<RowDataPacket[]>(
      "SELECT * FROM characters WHERE campaign_id = ? ORDER BY name",
      [campaignId]
    );
    const enriched = await enrichCharacters(characters);
    res.json(enriched);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// POST /api/campaigns/:campaignId/characters
router.post<CampaignParams>("/", requireAuth, async (req, res) => {
  try {
    const { campaignId } = req.params;
    const {
      name,
      class: charClass,
      ancestry,
      hp_max,
      alignment,
      background,
      deity,
      title,
      gp,
      stats,
    } = req.body;

    if (!name?.trim()) {
      res.status(400).json({ error: "Character name is required" });
      return;
    }

    const userId = req.user?.userId ?? null;

    const [result] = await db.execute<ResultSetHeader>(
      `INSERT INTO characters
         (campaign_id, user_id, name, class, ancestry, hp_current, hp_max,
          alignment, background, deity, title, gp, color)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
      [
        campaignId,
        userId,
        name.trim(),
        charClass ?? "Fighter",
        ancestry ?? "Human",
        hp_max ?? 1,
        hp_max ?? 1,
        alignment ?? "Neutral",
        background ?? "",
        deity ?? "",
        title ?? "",
        gp ?? 0,
        generateCharacterColor(),
      ]
    );
    const characterId = result.insertId;

    const statNames = ["STR", "DEX", "CON", "INT", "WIS", "CHA"];
    const providedStats: Record<string, number> =
      stats && typeof stats === "object" ? (stats as Record<string, number>) : {};

    await Promise.all(
      statNames.map((stat) =>
        db.execute(
          "INSERT INTO character_stats (character_id, stat_name, value) VALUES (?, ?, ?)",
          [characterId, stat, providedStats[stat] ?? 10]
        )
      )
    );

    const [charRows] = await db.execute<RowDataPacket[]>(
      "SELECT * FROM characters WHERE id = ?",
      [characterId]
    );
    const enriched = {
      ...charRows[0],
      overrides: {},
      stats: statNames.map((s) => ({ stat_name: s, value: providedStats[s] ?? 10 })),
      gear: [],
      talents: [],
    };

    const io: Server = req.app.get("io");
    broadcastToCampaign(io, Number(campaignId), "character:created", enriched);
    res.status(201).json(enriched);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// PATCH /api/characters/:id
router.patch("/:id", requireAuth, async (req, res) => {
  try {
    const { id } = req.params;

    const allowed = await canModifyCharacter(String(id), req.user!.userId);
    if (!allowed) {
      res.status(403).json({ error: "Not authorized to modify this character" });
      return;
    }

    const allowedFields = [
      "name", "class", "ancestry", "level", "xp", "hp_current", "hp_max",
      "ac", "alignment", "title", "notes", "background", "deity", "languages",
      "gp", "sp", "cp", "gear_slots_max", "overrides", "color", "luck_token",
      "torch_lit_at",
    ];

    const updates: string[] = [];
    const values: unknown[] = [];

    for (const field of allowedFields) {
      if (req.body[field] !== undefined) {
        updates.push(`${field} = ?`);
        const val = req.body[field];
        values.push(typeof val === "object" && val !== null ? JSON.stringify(val) : val);
      }
    }

    if (updates.length === 0) {
      res.status(400).json({ error: "No valid fields to update" });
      return;
    }

    values.push(id);
    const [updateResult] = await db.execute<ResultSetHeader>(`UPDATE characters SET ${updates.join(", ")} WHERE id = ?`, values as ExecuteValues);
    if (updateResult.affectedRows === 0) {
      res.status(404).json({ error: "Character not found" });
      return;
    }

    const [rows] = await db.execute<RowDataPacket[]>(
      "SELECT * FROM characters WHERE id = ?",
      [id]
    );

    const enriched = {
      ...rows[0],
      overrides: parseJson(rows[0].overrides),
    };

    const io: Server = req.app.get("io");
    broadcastToCampaign(io, Number(rows[0].campaign_id), "character:updated", enriched);
    res.json(enriched);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// DELETE /api/characters/:id
router.delete("/:id", requireAuth, async (req, res) => {
  try {
    const [rows] = await db.execute<RowDataPacket[]>(
      "SELECT * FROM characters WHERE id = ?",
      [req.params.id]
    );
    if (rows.length === 0) {
      res.status(404).json({ error: "Character not found" });
      return;
    }

    const allowed = await canModifyCharacter(String(req.params.id), req.user!.userId);
    if (!allowed) {
      res.status(403).json({ error: "Not authorized to delete this character" });
      return;
    }

    await db.execute("DELETE FROM characters WHERE id = ?", [req.params.id]);

    const io: Server = req.app.get("io");
    broadcastToCampaign(io, Number(rows[0].campaign_id), "character:deleted", {
      id: Number(req.params.id),
    });
    res.status(204).end();
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// PATCH /api/characters/:id/stats/:statName
router.patch("/:id/stats/:statName", async (req, res) => {
  try {
    const { id, statName } = req.params;
    const { value } = req.body;
    const upper = statName.toUpperCase();

    if (!DEFAULT_STATS.includes(upper)) {
      res.status(400).json({ error: "Invalid stat name" });
      return;
    }

    await db.execute(
      "UPDATE character_stats SET value = ? WHERE character_id = ? AND stat_name = ?",
      [value, id, upper]
    );

    const [rows] = await db.execute<RowDataPacket[]>(
      "SELECT campaign_id FROM characters WHERE id = ?",
      [id]
    );
    if (rows.length === 0) {
      res.status(404).json({ error: "Character not found" });
      return;
    }

    const io: Server = req.app.get("io");
    broadcastToCampaign(io, Number(rows[0].campaign_id), "stat:updated", {
      characterId: Number(id),
      statName: upper,
      value,
    });
    res.json({ characterId: Number(id), statName: upper, value });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// POST /api/characters/:id/gear
router.post("/:id/gear", async (req, res) => {
  try {
    const { id } = req.params;
    const { name, type, slot_count, properties, effects, game_item_id } = req.body;

    if (!name?.trim()) {
      res.status(400).json({ error: "Gear name is required" });
      return;
    }

    const [result] = await db.execute<ResultSetHeader>(
      `INSERT INTO character_gear
         (character_id, name, type, slot_count, properties, effects, game_item_id)
       VALUES (?, ?, ?, ?, ?, ?, ?)`,
      [
        id, name.trim(), type ?? "gear", slot_count ?? 1,
        JSON.stringify(properties ?? {}),
        JSON.stringify(effects ?? {}),
        game_item_id ?? null,
      ]
    );

    const [gearRows] = await db.execute<RowDataPacket[]>(
      "SELECT * FROM character_gear WHERE id = ?",
      [result.insertId]
    );
    const gear = {
      ...gearRows[0],
      properties: parseJson(gearRows[0].properties),
      effects: parseJson(gearRows[0].effects),
    };

    const [charRows] = await db.execute<RowDataPacket[]>(
      "SELECT campaign_id FROM characters WHERE id = ?",
      [id]
    );
    const io: Server = req.app.get("io");
    broadcastToCampaign(io, Number(charRows[0].campaign_id), "gear:added", {
      characterId: Number(id),
      gear,
    });
    res.status(201).json(gear);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// DELETE /api/characters/:id/gear/:gearId
router.delete("/:id/gear/:gearId", async (req, res) => {
  try {
    const { id, gearId } = req.params;
    const [charRows] = await db.execute<RowDataPacket[]>(
      "SELECT campaign_id FROM characters WHERE id = ?",
      [id]
    );
    if (charRows.length === 0) {
      res.status(404).json({ error: "Character not found" });
      return;
    }

    const [result] = await db.execute<ResultSetHeader>(
      "DELETE FROM character_gear WHERE id = ? AND character_id = ?",
      [gearId, id]
    );
    if (result.affectedRows === 0) {
      res.status(404).json({ error: "Gear not found" });
      return;
    }

    const io: Server = req.app.get("io");
    broadcastToCampaign(io, Number(charRows[0].campaign_id), "gear:removed", {
      characterId: Number(id),
      gearId: Number(gearId),
    });
    res.status(204).end();
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// POST /api/characters/:id/talents
router.post("/:id/talents", async (req, res) => {
  try {
    const { id } = req.params;
    const { name, description, effect, game_talent_id } = req.body;

    if (!name?.trim()) {
      res.status(400).json({ error: "Talent name is required" });
      return;
    }

    const [result] = await db.execute<ResultSetHeader>(
      `INSERT INTO character_talents
         (character_id, name, description, effect, game_talent_id)
       VALUES (?, ?, ?, ?, ?)`,
      [id, name.trim(), description ?? "", JSON.stringify(effect ?? {}), game_talent_id ?? null]
    );

    const [talentRows] = await db.execute<RowDataPacket[]>(
      "SELECT * FROM character_talents WHERE id = ?",
      [result.insertId]
    );
    const talent = { ...talentRows[0], effect: parseJson(talentRows[0].effect) };

    const [charRows] = await db.execute<RowDataPacket[]>(
      "SELECT campaign_id FROM characters WHERE id = ?",
      [id]
    );
    const io: Server = req.app.get("io");
    broadcastToCampaign(io, Number(charRows[0].campaign_id), "talent:added", {
      characterId: Number(id),
      talent,
    });
    res.status(201).json(talent);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

// DELETE /api/characters/:id/talents/:talentId
router.delete("/:id/talents/:talentId", async (req, res) => {
  try {
    const { id, talentId } = req.params;
    const [charRows] = await db.execute<RowDataPacket[]>(
      "SELECT campaign_id FROM characters WHERE id = ?",
      [id]
    );
    if (charRows.length === 0) {
      res.status(404).json({ error: "Character not found" });
      return;
    }

    const [result] = await db.execute<ResultSetHeader>(
      "DELETE FROM character_talents WHERE id = ? AND character_id = ?",
      [talentId, id]
    );
    if (result.affectedRows === 0) {
      res.status(404).json({ error: "Talent not found" });
      return;
    }

    const io: Server = req.app.get("io");
    broadcastToCampaign(io, Number(charRows[0].campaign_id), "talent:removed", {
      characterId: Number(id),
      talentId: Number(talentId),
    });
    res.status(204).end();
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Internal server error" });
  }
});

const MISHAP_TABLE = [
  null,
  { id: 1, description: "Devastation! Roll twice, combine both effects." },
  { id: 2, description: "Explosion! Take 1d8 damage.", mechanic: "damage_1d8" },
  { id: 3, description: "Refraction! The spell targets you instead.", mechanic: "narrative" },
  { id: 4, description: "Your hand slipped! The spell hits a random ally.", mechanic: "narrative" },
  { id: 5, description: "Mind wound! You cannot cast this spell again for a week.", mechanic: "lock_week" },
  { id: 6, description: "Discorporation! One random piece of gear disappears forever.", mechanic: "remove_gear" },
  { id: 7, description: "Spell worm! Lose a random spell each turn until you pass a DC 12 CON check.", mechanic: "condition", condition: "Spell Worm" },
  { id: 8, description: "Harmonic failure! Lose a random known spell until rest.", mechanic: "exhaust_random" },
  { id: 9, description: "Poof! All light suppressed within 30ft for 10 rounds.", mechanic: "condition", condition: "Light Suppressed" },
  { id: 10, description: "The horror! You scream uncontrollably for 3 rounds, drawing attention.", mechanic: "condition", condition: "Screaming" },
  { id: 11, description: "Energy surge! You glow bright purple for 10 rounds. Enemies have advantage on attacks.", mechanic: "condition", condition: "Glowing Purple" },
  { id: 12, description: "Unstable conduit! Disadvantage on casting same-tier spells for 10 rounds.", mechanic: "condition", condition: "Unstable Conduit" },
] as const;

async function applyWizardMishap(characterId: number, campaignId: number, changes: object[]): Promise<object> {
  const roll = Math.floor(Math.random() * 12) + 1;
  const mishap = MISHAP_TABLE[roll];
  if (!mishap) return { roll, description: "Unknown mishap" };

  if (roll === 1) {
    const r1 = await applyWizardMishap(characterId, campaignId, changes);
    const r2 = await applyWizardMishap(characterId, campaignId, changes);
    return { roll: 1, description: (MISHAP_TABLE[1] as { description: string }).description, combined: [r1, r2] };
  }

  if ((mishap as { mechanic?: string }).mechanic === "damage_1d8") {
    const dmg = Math.floor(Math.random() * 8) + 1;
    const [charRows] = await db.execute<RowDataPacket[]>("SELECT hp_current FROM characters WHERE id = ?", [characterId]);
    const prevHp = (charRows[0] as { hp_current: number }).hp_current;
    const newHp = Math.max(0, prevHp - dmg);
    await db.execute("UPDATE characters SET hp_current = ? WHERE id = ?", [newHp, characterId]);
    changes.push({ type: "hp_change", characterId, delta: -dmg, previous: prevHp });
    return { roll, description: `${(mishap as {description:string}).description} (${dmg} damage)`, damage: dmg };
  }

  if ((mishap as { mechanic?: string }).mechanic === "remove_gear") {
    const [gearRows] = await db.execute<RowDataPacket[]>("SELECT * FROM character_gear WHERE character_id = ? ORDER BY RAND() LIMIT 1", [characterId]);
    if (gearRows.length > 0) {
      const gear = gearRows[0];
      await db.execute("DELETE FROM character_gear WHERE id = ?", [gear.id]);
      changes.push({ type: "gear_removed", gear });
    }
    return { roll, description: (mishap as {description:string}).description };
  }

  if ((mishap as { mechanic?: string }).mechanic === "exhaust_random") {
    const [spellRows] = await db.execute<RowDataPacket[]>("SELECT id FROM character_spells WHERE character_id = ? AND exhausted = 0 ORDER BY RAND() LIMIT 1", [characterId]);
    if (spellRows.length > 0) {
      await db.execute("UPDATE character_spells SET exhausted = 1 WHERE id = ?", [spellRows[0].id]);
      changes.push({ type: "spell_exhausted", characterSpellId: spellRows[0].id });
    }
    return { roll, description: (mishap as {description:string}).description };
  }

  if ((mishap as { mechanic?: string }).mechanic === "condition" && (mishap as { condition?: string }).condition) {
    const [condResult] = await db.execute<import("mysql2").ResultSetHeader>(
      "INSERT INTO character_conditions (character_id, name, description) VALUES (?, ?, ?)",
      [characterId, (mishap as {condition:string}).condition, (mishap as {description:string}).description]
    );
    changes.push({ type: "condition_added", conditionId: condResult.insertId, name: (mishap as {condition:string}).condition });
    return { roll, description: (mishap as {description:string}).description };
  }

  return { roll, description: (mishap as {description:string}).description };
}

// POST /api/characters/:id/spells/:spellId/cast
router.post("/:id/spells/:spellId/cast", requireAuth, async (req, res, next) => {
  try {
    const characterId = Number(req.params.id);
    const spellId = Number(req.params.spellId);

    const [spellRows] = await db.execute<RowDataPacket[]>(
      "SELECT s.*, cs.id as cs_id, cs.exhausted FROM spells s JOIN character_spells cs ON cs.spell_id = s.id WHERE s.id = ? AND cs.character_id = ?",
      [spellId, characterId]
    );
    if (spellRows.length === 0) return void res.status(404).json({ error: "Spell not known" });
    const spell = spellRows[0];
    if (spell.exhausted) return void res.status(400).json({ error: "Spell is exhausted" });

    const [charRows] = await db.execute<RowDataPacket[]>(
      "SELECT c.id, c.campaign_id, c.name, c.class, c.color, cs2.value as stat_value FROM characters c JOIN character_stats cs2 ON cs2.character_id = c.id WHERE c.id = ? AND cs2.stat_name = ?",
      [characterId, spell.casting_stat]
    );
    if (charRows.length === 0) return void res.status(404).json({ error: "Character not found" });
    const character = charRows[0];
    const statMod = Math.floor((character.stat_value - 10) / 2);

    const roll = Math.floor(Math.random() * 20) + 1;
    const total = roll + statMod;
    const dc = 10 + spell.tier;
    const isCritSuccess = roll === 20;
    const isCritFail = roll === 1;
    const isSuccess = isCritSuccess || (!isCritFail && total >= dc);

    let result: "success" | "failure" | "crit_success" | "crit_fail" = "success";
    if (isCritSuccess) result = "crit_success";
    else if (isCritFail) result = "crit_fail";
    else if (!isSuccess) result = "failure";

    const changes: object[] = [];

    if (!isSuccess) {
      await db.execute("UPDATE character_spells SET exhausted = 1 WHERE id = ?", [spell.cs_id]);
      changes.push({ type: "spell_exhausted", characterSpellId: spell.cs_id });
    }

    let mishapResult: object | null = null;
    if (isCritFail && character.class === "Wizard") {
      mishapResult = await applyWizardMishap(characterId, character.campaign_id, changes);
    }

    if (isCritFail && character.class === "Priest") {
      changes.push({ type: "priest_penance", characterSpellId: spell.cs_id, tier: spell.tier });
    }

    const metadata = JSON.stringify({ spellId, spellName: spell.name, result, mishapResult, changes });
    const [logResult] = await db.execute<import("mysql2").ResultSetHeader>(
      "INSERT INTO roll_log (campaign_id, character_id, character_name, character_color, type, subtype, label, dice_expression, rolls, modifier, total, nat20, metadata) VALUES (?, ?, ?, ?, 'custom', 'spell_cast', ?, '1d20', ?, ?, ?, ?, ?)",
      [character.campaign_id, characterId, character.name, character.color, `${spell.name} (Tier ${spell.tier})`, JSON.stringify([roll]), statMod, total, isCritSuccess ? 1 : 0, metadata]
    );

    const io = req.app.get("io");
    io.to(String(character.campaign_id)).emit("spell:cast", {
      rollId: logResult.insertId,
      characterId,
      spellId,
      spellName: spell.name,
      roll,
      modifier: statMod,
      total,
      dc,
      result,
      mishapResult,
      changes,
    });

    res.json({ rollId: logResult.insertId, roll, modifier: statMod, total, dc, result, mishapResult });
  } catch (err) { next(err); }
});

async function getCharacterCampaignId(id: number): Promise<{ campaign_id: number }> {
  const [rows] = await db.execute<RowDataPacket[]>("SELECT campaign_id FROM characters WHERE id = ?", [id]);
  return rows[0] as { campaign_id: number };
}

// GET /api/characters/:id/spells
router.get("/:id/spells", requireAuth, async (req, res, next) => {
  try {
    const [rows] = await db.execute<RowDataPacket[]>(
      `SELECT cs.id, cs.spell_id, cs.exhausted, cs.locked_until, cs.focus_active, cs.focus_started_at,
              s.name, s.class, s.tier, s.casting_stat, s.duration, s.\`range\`, s.is_focus, s.description
       FROM character_spells cs
       JOIN spells s ON s.id = cs.spell_id
       WHERE cs.character_id = ?
       ORDER BY s.tier, s.name`,
      [req.params.id]
    );
    res.json(rows);
  } catch (err) { next(err); }
});

// POST /api/characters/:id/spells
router.post("/:id/spells", requireAuth, async (req, res, next) => {
  try {
    const { spell_id } = req.body;
    await db.execute(
      "INSERT IGNORE INTO character_spells (character_id, spell_id) VALUES (?, ?)",
      [req.params.id, spell_id]
    );
    const [rows] = await db.execute<RowDataPacket[]>(
      `SELECT cs.*, s.name, s.class, s.tier, s.casting_stat, s.duration, s.\`range\`, s.is_focus, s.description
       FROM character_spells cs JOIN spells s ON s.id = cs.spell_id
       WHERE cs.character_id = ? AND cs.spell_id = ?`,
      [req.params.id, spell_id]
    );
    const io = req.app.get("io");
    const char = await getCharacterCampaignId(Number(req.params.id));
    io.to(String(char.campaign_id)).emit("spell:added", { characterId: Number(req.params.id), spell: rows[0] });
    res.status(201).json(rows[0]);
  } catch (err) { next(err); }
});

// DELETE /api/characters/:id/spells/:spellId
router.delete("/:id/spells/:spellId", requireAuth, async (req, res, next) => {
  try {
    await db.execute(
      "DELETE FROM character_spells WHERE character_id = ? AND spell_id = ?",
      [req.params.id, req.params.spellId]
    );
    const io = req.app.get("io");
    const char = await getCharacterCampaignId(Number(req.params.id));
    io.to(String(char.campaign_id)).emit("spell:removed", { characterId: Number(req.params.id), spellId: Number(req.params.spellId) });
    res.json({ ok: true });
  } catch (err) { next(err); }
});

// POST /api/characters/:id/rest
router.post("/:id/rest", requireAuth, async (req, res, next) => {
  try {
    await db.execute(
      "UPDATE character_spells SET exhausted = 0, focus_active = 0, focus_started_at = NULL WHERE character_id = ? AND locked_until IS NULL",
      [req.params.id]
    );
    await db.execute(
      "DELETE FROM character_conditions WHERE character_id = ?",
      [req.params.id]
    );
    const io = req.app.get("io");
    const char = await getCharacterCampaignId(Number(req.params.id));
    io.to(String(char.campaign_id)).emit("character:rested", { characterId: Number(req.params.id) });
    res.json({ ok: true });
  } catch (err) { next(err); }
});

export default router;
