package inf101.v18.rogue101.game;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.function.Supplier;

import inf101.v18.gfx.Screen;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.gfxmode.TurtlePainter;
import inf101.v18.gfx.textmode.Printer;
import inf101.v18.grid.GridDirection;
import inf101.v18.grid.IGrid;
import inf101.v18.grid.ILocation;
import inf101.v18.rogue101.Main;
import inf101.v18.rogue101.enemies.Boss;
import inf101.v18.rogue101.enemies.Girl;
import inf101.v18.rogue101.examples.Carrot;
import inf101.v18.rogue101.examples.Manga;
import inf101.v18.rogue101.items.*;
import inf101.v18.rogue101.examples.Rabbit;
import inf101.v18.rogue101.map.GameMap;
import inf101.v18.rogue101.map.IGameMap;
import inf101.v18.rogue101.map.IMapView;
import inf101.v18.rogue101.map.MapReader;
import inf101.v18.rogue101.objects.*;
import inf101.v18.rogue101.shared.NPC;
import inf101.v18.rogue101.states.Attack;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.KeyCode;
import javafx.scene.paint.Color;

public class Game implements IGame {
	/**
	 * All the IActors that have things left to do this turn
	 */
	private List<IActor> actors = Collections.synchronizedList(new ArrayList<>());
	/**
	 * For fancy solution to factory problem
	 */
	private Map<String, Supplier<IItem>> itemFactories = new HashMap<>();
	/**
	 * Useful random generator
	 */
	private Random random = new Random();

    /**
     * Saves the last three messages
     */
    private List<String> lastMessages = new ArrayList<>();

	/**
	 * The game map. {@link IGameMap} gives us a few more details than
	 * {@link IMapView} (write access to item lists); the game needs this but
	 * individual items don't.
	 */
	private IGameMap map;
	private List<IGameMap> maps = new ArrayList<>();
	private int currentLVL = 0;
	private IActor currentActor;
	private ILocation currentLocation;
	private int movePoints = 0;
	private final ITurtle painter;
	private final Printer printer;
	private int numPlayers = 0;

	public Game(Screen screen, ITurtle painter, Printer printer) {
		this.painter = painter;
		this.printer = printer;

		addFactory();

		// NOTE: in a more realistic situation, we will have multiple levels (one map
		// per level), and (at least for a Roguelike game) the levels should be
		// generated
		//
		// inputGrid will be filled with single-character strings indicating what (if
		// anything)
		// should be placed at that map square
        loadMap("level1.txt", 1);
        loadMap("level2.txt", 2);
        loadMap("level3.txt", 3);
        loadMap("level4.txt", 4);
        loadMap("level5.txt", 5);
        map = maps.get(0);
        map.add(map.getLocation( 3, 3), new Player()); //We must be sure there is never more than one player

		// Prints some helpful information.
		String[] info = {"Controls:", "WASD or arrow keys for movement", "E to pick up an item", "Q to drop an item", "1-0 to choose an item (10=0)", "N to change name", "ENTER to confirm", "R to use a ranged attack", "F to use a magic attack", "C to use a potion"};
        for (int i = 0; i < info.length; i++) {
            this.printer.printAt(Main.COLUMN_MAP_END + 2, 1 + i, info[i]);
        }
	}

	public Game(String mapString) {
		printer = new Printer(1280, 720);
		painter = new TurtlePainter(1280, 720);

		addFactory();

		IGrid<String> inputGrid = MapReader.readString(mapString);
		this.map = new GameMap(inputGrid.getArea());
		for (ILocation loc : inputGrid.locations()) {
			IItem item = createItem(inputGrid.get(loc));
			if (item != null) {
				map.add(loc, item);
			}
		}
	}

	@Override
	public void addItem(IItem item) {
		map.add(currentLocation, item);
	}

	@Override
	public void addItem(String sym) {
		IItem item = createItem(sym);
		if (item != null)
			map.add(currentLocation, item);
	}

	/**
	 * Calculates the attack of an IActor based on equipped items.
	 *
	 * @return	The attack
	 */
	private int getAttack() {
		int damage = currentActor.getAttack() + random.nextInt(20) + 1;
		IBuffItem buff = (IBuffItem)currentActor.getItem(IBuffItem.class);
		if (buff != null) {
			damage += buff.getBuffDamage();
		}
		return damage;
	}

	/**
	 * Gets the defence of the current target.
	 *
	 * @param target	The target to evaluate
	 * @return			The defence of the target
	 */
	private int getDefence(IItem target) {
		int defence = target.getDefence() + 10;
		IActor actor = (IActor) target;
		IBuffItem item = (IBuffItem) actor.getItem(IBuffItem.class);
		if (item != null) {
			defence += item.getBuffDefence();
		}
		return defence;
	}

	public void goUp() {
	    if (currentLVL + 1 < maps.size()) {
            map = maps.get(++currentLVL);
            if (!map.has(currentLocation, currentActor)) {
                map.add(currentLocation, currentActor);
            }
            actors = new ArrayList<>();
            displayMessage("You went up the stairs");
        }
    }

    public void goDown() {
        if (currentLVL > 0) {
            map = maps.get(--currentLVL);
            if (!map.has(currentLocation, currentActor)) {
                map.add(currentLocation, currentActor);
            }
            actors = new ArrayList<>();
            displayMessage("You went down the stairs");
        }
    }

	/**
	 * Gets the damage done against the current target.
	 *
	 * @param target	The target to evaluate.
	 * @return			The damage done to the target.
	 */
	private int getDamage(IItem target) {
		int damage = currentActor.getDamage();
        IWeapon weapon = (IWeapon)currentActor.getItem(IWeapon.class);
        if (weapon != null) {
            damage += weapon.getWeaponDamage();
        }
		IActor actor = (IActor) target;
		IBuffItem item = (IBuffItem) actor.getItem(IBuffItem.class);
		if (item != null) {
			damage -= item.getBuffDamageReduction();
		}
		return damage;
	}

	@Override
	public ILocation attack(GridDirection dir, IItem target) {
		ILocation loc = currentLocation.go(dir);
		if (!map.has(loc, target)) {
			throw new IllegalMoveException("Target isn't there!");
		}
        IWeapon weapon = (IWeapon) currentActor.getItem(IMeleeWeapon.class);
		if (weapon != null) {
			NPC.playSound(weapon.getSound());
		} else {
			NPC.playSound("audio/Realistic_Punch-Mark_DiAngelo-1609462330.wav");
		}
		if (getAttack() >= getDefence(target)) {
			int actualDamage = target.handleDamage(this, target, getDamage(target));
			if (currentActor != null) {
                formatMessage("%s hits %s for %d damage", currentActor.getName(), target.getName(), actualDamage);
            }
		} else {
			formatMessage("%s tried to hit %s, but missed", currentActor.getName(), target.getName());
		}
		
		map.clean(loc);

		if (target.isDestroyed()) {
			return move(dir);
		} else {
			movePoints--;
			return currentLocation;
		}
	}

	@Override
	public ILocation rangedAttack(GridDirection dir, IItem target, Attack type) {
		ILocation loc = currentLocation;
		IWeapon weapon = null;
        switch (type) {
            case MAGIC:
                weapon = (IWeapon) currentActor.getItem(IMagicWeapon.class);
                break;
            case RANGED:
                weapon = (IWeapon) currentActor.getItem(IRangedWeapon.class);
        }
		if (weapon != null) {
			NPC.playSound(weapon.getSound());
		} else {
			NPC.playSound("audio/Snow Ball Throw And Splat-SoundBible.com-992042947.wav");
		}
		if (getAttack() >= getDefence(target)) {
			int damage = getDamage(target) / loc.gridDistanceTo(map.getLocation(target));
			int actualDamage = target.handleDamage(this, target, damage);
			formatMessage("%s hits %s for %d damage", currentActor.getName(), target.getName(), actualDamage);
		} else {
			formatMessage("%s tried to hit %s, but missed", currentActor.getName(), target.getName());
		}
		map.clean(map.getLocation(target));
		if (target.isDestroyed() && map.has(currentLocation.go(dir), target)) {
			return move(dir);
		} else {
			return currentLocation;
		}
	}

	/**
	 * Begin a new game turn, or continue to the previous turn
	 * 
	 * @return True if the game should wait for more user input
	 */
	public boolean doTurn() {
		do {
			if (actors.isEmpty()) {
				// System.err.println("new turn!");

				// no one in the queue, we're starting a new turn!
				// first collect all the actors:
				beginTurn();
			}

			/*if (random.nextInt(100) < 20) {
				ILocation loc = map.getLocation(random.nextInt(map.getWidth()), random.nextInt(map.getHeight()));
				if (!map.hasActors(loc) && !map.hasItems(loc) && !map.hasWall(loc)) {
					map.add(loc, new Carrot());
				}
			}*/ //We don't want this in the actual game.

			// process actors one by one; for the IPlayer, we return and wait for keypresses
			// Possible for INonPlayer, we could also return early (returning
			// *false*), and then insert a little timer delay between each non-player move
			// (the timer
			// is already set up in Main)
           if (numPlayers == 0) {
               kill();
           }
			while (!actors.isEmpty()) {
                // get the next player or non-player in the queue
                currentActor = actors.remove(0);
                if (currentActor == null) {
                    return false; //TODO: Find out why a girl suddenly becomes null (only after map change, not caught in beginTurn)
                }
                if (currentActor.isDestroyed()) // skip if it's dead
                    continue;
				currentLocation = map.getLocation(currentActor);
				if (currentLocation == null) {
					displayDebug("doTurn(): Whoops! Actor has disappeared from the map: " + currentActor);
				}
				movePoints = 1; // everyone gets to do one thing

				if (currentActor instanceof INonPlayer) {
					// computer-controlled players do their stuff right away
					((INonPlayer) currentActor).doTurn(this);
					// remove any dead items from current location
					map.clean(currentLocation);
					return false;
				} else if (currentActor instanceof IPlayer) {
                    if (!currentActor.isDestroyed()) {
						// For the human player, we need to wait for input, so we just return.
						// Further keypresses will cause keyPressed() to be called, and once the human
						// makes a move, it'll lose its movement point and doTurn() will be called again
						//
						// NOTE: currentActor and currentLocation are set to the IPlayer (above),
						// so the game remembers who the player is whenever new keypresses occur. This
						// is also how e.g., getLocalItems() work – the game always keeps track of
						// whose turn it is.
						return true;
					}
				} else {
					displayDebug("doTurn(): Hmm, this is a very strange actor: " + currentActor);
				}
			}
		} while (numPlayers > 0); // we can safely repeat if we have players, since we'll return (and break out of
									// the loop) once we hit the player
		return true;
	}

    /**
     * Player is dead. It needs to be properly killed off.
     */
	private void kill() {
        map.remove(currentLocation, currentActor);
        currentActor = null;
        currentLocation = null;
        actors = new ArrayList<>();
        loadMap("gameover.txt");
        NPC.playSound("audio/Dying-SoundBible.com-1255481835.wav");
    }

    /**
     * Loads a map with the desired name
     *
     * @param mapName Name of map, including extension.
     */
    private void loadMap(String mapName) {
        IGrid<String> inputGrid = MapReader.readFile("maps/" + mapName);
        if (inputGrid == null) {
            System.err.println("Map not found – falling back to builtin map");
            inputGrid = MapReader.readString(Main.BUILTIN_MAP);
        }
        IGameMap map = new GameMap(inputGrid.getArea());
        for (ILocation loc : inputGrid.locations()) {
            IItem item = createItem(inputGrid.get(loc));
            if (item != null) {
                map.add(loc, item);
            }
        }
        this.map = map;
    }

    private void loadMap(String mapName, int lvl) {
        IGrid<String> inputGrid = MapReader.readFile("maps/" + mapName);
        if (inputGrid == null) {
            System.err.println("Map not found – falling back to builtin map");
            inputGrid = MapReader.readString(Main.BUILTIN_MAP);
        }
        IGameMap map = new GameMap(inputGrid.getArea());
        for (ILocation loc : inputGrid.locations()) {
            IItem item = createItem(inputGrid.get(loc));
            if (item instanceof Chest) {
                ((Chest) item).fill(lvl);
            }
            if (item != null) {
                map.add(loc, item);
            }
        }
        maps.add(map);
    }

	/**
	 * Go through the map and collect all the actors.
	 */
	private void beginTurn() {
		numPlayers = 0;
		// this extra fancy iteration over each map location runs *in parallel* on
		// multicore systems!
		// that makes some things more tricky, hence the "synchronized" block and
		// "Collections.synchronizedList()" in the initialization of "actors".
		// NOTE: If you want to modify this yourself, it might be a good idea to replace
		// "parallelStream()" by "stream()", because weird things can happen when many
		// things happen
		// at the same time! (or do INF214 or DAT103 to learn about locks and threading)
		map.getArea().parallelStream().forEach((loc) -> { // will do this for each location in map
			List<IItem> list = map.getAllModifiable(loc); // all items at loc
			Iterator<IItem> li = list.iterator(); // manual iterator lets us remove() items
			while (li.hasNext()) { // this is what "for(IItem item : list)" looks like on the inside
				IItem item = li.next();
				if (item.getCurrentHealth() < 0) {
					// normally, we expect these things to be removed when they are destroyed, so
					// this shouldn't happen
					synchronized (this) {
						formatDebug("beginTurn(): found and removed leftover destroyed item %s '%s' at %s%n",
								item.getName(), item.getSymbol(), loc);
					}
					li.remove();
					map.remove(loc, item); // need to do this too, to update item map
				} else if (item instanceof IPlayer) {
					actors.add(0, (IActor) item); // we let the human player go first
					synchronized (this) {
						numPlayers++;
					}
				} else if (item instanceof IActor) {
					actors.add((IActor) item); // add other actors to the end of the list
				} else if (item instanceof Carrot) {
					((Carrot) item).doTurn();
				}
			}
		});
	}

	@Override
	public boolean canGo(GridDirection dir) {
		return map.canGo(currentLocation, dir);
	}

	private void addFactory() {
		itemFactories.put("#", Wall::new);
		itemFactories.put("@", Player::new);
		itemFactories.put("C", Carrot::new);
		itemFactories.put("R", Rabbit::new);
		itemFactories.put("M", Manga::new);
		itemFactories.put("G", Girl::new);
		itemFactories.put(".", Dust::new);
		itemFactories.put("S", Sword::new);
        itemFactories.put("c", Chest::new);
		itemFactories.put("B", Boss::new);
		itemFactories.put("s", Staff::new);
		itemFactories.put("b", Bow::new);
        itemFactories.put("H", HealthPotion::new);
        itemFactories.put("=", FakeWall::new);
        itemFactories.put("<", StairsUp::new);
        itemFactories.put(">", StairsDown::new);
        itemFactories.put("m", Binoculars::new);
        itemFactories.put("f", Shield::new);
	}

	@Override
	public IItem createItem(String sym) {
		switch (sym) {
		case " ":
			return null;
		default:
			// alternative/advanced method
			Supplier<IItem> factory = itemFactories.get(sym);
			if (factory != null) {
				return factory.get();
			} else {
				System.err.println("createItem: Don't know how to create a '" + sym + "'");
				return null;
			}
		}
	}

	@Override
	public void displayDebug(String s) {
		printer.clearLine(Main.LINE_DEBUG);
		printer.printAt(1, Main.LINE_DEBUG, s, Color.DARKRED);
		System.err.println(s);
	}

	@Override
	public void displayMessage(String s) {
        if (lastMessages.size() >= 3) {
            lastMessages.remove(2);
        }
        lastMessages.add(0, s);
		printer.clearLine(Main.LINE_MSG1);
        printer.clearLine(Main.LINE_MSG2);
        printer.clearLine(Main.LINE_MSG3);

		int maxLen = 80; //The maximum length of a message to not overflow.
		boolean secondLineWritten = false;
		boolean thirdLineWritten = false;

        if (lastMessages.size() > 0) {
        	String message = lastMessages.get(0);
        	if (message.length() > 2 * maxLen) {
				printer.printAt(1, Main.LINE_MSG1, message.substring(0, maxLen));
				printer.printAt(1, Main.LINE_MSG2, message.substring(maxLen, 2 * maxLen));
				printer.printAt(1, Main.LINE_MSG3, message.substring(2 * maxLen));
				secondLineWritten = thirdLineWritten = true;
			} else if (message.length() > maxLen) {
				printer.printAt(1, Main.LINE_MSG1, message.substring(0, maxLen));
				printer.printAt(1, Main.LINE_MSG2, message.substring(maxLen));
				secondLineWritten = true;
			} else {
				printer.printAt(1, Main.LINE_MSG1, message);
			}
        }
        if (lastMessages.size() > 1 && !secondLineWritten) {
			String message = lastMessages.get(1);
			if (message.length() > maxLen) {
				printer.printAt(1, Main.LINE_MSG2, message.substring(0, maxLen));
				printer.printAt(1, Main.LINE_MSG3, message.substring(maxLen));
				thirdLineWritten = true;
			} else {
				printer.printAt(1, Main.LINE_MSG2, message);
			}
        }
        if (lastMessages.size() > 2 && !thirdLineWritten) {
            printer.printAt(1, Main.LINE_MSG3, lastMessages.get(2));
        }
		System.out.println("Message: «" + s + "»");
	}

	@Override
	public void displayStatus(String s) {
		printer.clearLine(Main.LINE_STATUS);
		printer.printAt(1, Main.LINE_STATUS, s);
		System.out.println("Status: «" + s + "»");
	}

	public void draw() {
	    if (numPlayers == 0) {
		    map.draw(painter, printer);
        } else {
            ((GameMap) map).drawVisible(painter, printer);
        }
	}

	@Override
	public boolean drop(IItem item) {
		if (item != null) {
			map.add(currentLocation, item);
			return true;
		} else
			return false;
	}

	@Override
	public boolean dropAt(ILocation loc, IItem item) {
		if (item != null) {
			map.add(loc, item);
			return true;
		} else
			return false;
	}

	@Override
	public void formatDebug(String s, Object... args) {
		displayDebug(String.format(s, args));
	}

	@Override
	public void formatMessage(String s, Object... args) {
		displayMessage(String.format(s, args));
	}

	@Override
	public void formatStatus(String s, Object... args) {
		displayStatus(String.format(s, args));
	}

	@Override
	public int getHeight() {
		return map.getHeight();
	}

	@Override
	public List<IItem> getLocalItems() {
		return map.getItems(currentLocation);
	}

	@Override
	public ILocation getLocation() {
		return currentLocation;
	}

	@Override
	public ILocation getLocation(GridDirection dir) {
		if (currentLocation.canGo(dir))
			return currentLocation.go(dir);
		else
			return null;
	}

	/**
	 * Return the game map. {@link IGameMap} gives us a few more details than
	 * {@link IMapView} (write access to item lists); the game needs this but
	 * individual items don't.
	 */
	@Override
	public IMapView getMap() {
		return map;
	}

	@Override
	public List<GridDirection> getPossibleMoves() {
		List<GridDirection> moves = new ArrayList<>();
		for (GridDirection dir : GridDirection.FOUR_DIRECTIONS) {
			if (canGo(dir)) {
				moves.add(dir);
			}
		}
		return moves;
	}

	@Override
	public List<ILocation> getVisible() {
		List<ILocation> neighbours = map.getNeighbourhood(currentLocation, currentActor.getVision());
		List<ILocation> invalid = new ArrayList<>();
		for (ILocation neighbour : neighbours) {
			for (ILocation tile : currentLocation.gridLineTo(neighbour)) {
				if (map.hasWall(tile)) {
					invalid.add(neighbour);
					break;
				}
			}
		}
		neighbours.removeAll(invalid);
		return neighbours;
	}

	@Override
	public int getWidth() {
		return map.getWidth();
	}

	public boolean keyPressed(KeyCode code) {
		// only an IPlayer/human can handle keypresses, and only if it's the human's
		// turn
		if (currentActor instanceof IPlayer) {
			return !((IPlayer) currentActor).keyPressed(this, code);
		}
		return true;
	}

	@Override
	public ILocation move(GridDirection dir) {
		if (movePoints < 1)
			throw new IllegalMoveException("You're out of moves!");
		ILocation newLoc = map.go(currentLocation, dir);
		map.remove(currentLocation, currentActor);
		map.add(newLoc, currentActor);
		currentLocation = newLoc;
		movePoints--;
		return currentLocation;
	}

	@Override
	public IItem pickUp(IItem item) {
		if (item != null && map.has(currentLocation, item) && !(item instanceof IStatic)) {
            if (item instanceof IActor) {
                if (item.getCurrentHealth() / item.getMaxHealth() < 3) {
                    map.remove(currentLocation, item);
                    return item;
                } else {
                    return null;
                }
            } else if (currentActor.getAttack() > item.getDefence()) {
                map.remove(currentLocation, item);
                return item;
            } else {
                return null;
            }
		} else {
			return null;
		}
	}

	@Override
	public ITurtle getPainter() {
		return painter;
	}

	@Override
	public Printer getPrinter() {
		return printer;
	}

	@Override
	public int[] getFreeTextAreaBounds() {
		int[] area = new int[4];
		area[0] = getWidth() + 1;
		area[1] = 1;
		area[2] = printer.getLineWidth();
		area[3] = printer.getPageHeight() - 5;
		return area;
	}

	@Override
	public void clearFreeTextArea() {
		printer.clearRegion(getWidth() + 1, 1, printer.getLineWidth() - getWidth(), printer.getPageHeight() - 5);
	}

	@Override
	public void clearFreeGraphicsArea() {
		painter.as(GraphicsContext.class).clearRect(getWidth() * printer.getCharWidth(), 0,
				painter.getWidth() - getWidth() * printer.getCharWidth(),
				(printer.getPageHeight() - 5) * printer.getCharHeight());
	}

	@Override
	public double[] getFreeGraphicsAreaBounds() {
		double[] area = new double[4];
		area[0] = getWidth() * printer.getCharWidth();
		area[1] = 0;
		area[2] = painter.getWidth();
		area[3] = getHeight() * printer.getCharHeight();
		return area;
	}

	@Override
	public IActor getActor() {
		return currentActor;
	}

	public ILocation setCurrent(IActor actor) {
		currentLocation = map.getLocation(actor);
		if (currentLocation != null) {
			currentActor = actor;
			movePoints = 1;
		}
		return currentLocation;
	}

	public IActor setCurrent(ILocation loc) {
		List<IActor> list = map.getActors(loc);
		if (!list.isEmpty()) {
			currentActor = list.get(0);
			currentLocation = loc;
			movePoints = 1;
		}
		return currentActor;
	}

	public IActor setCurrent(int x, int y) {
		return setCurrent(map.getLocation(x, y));
	}
	
	@Override
	public Random getRandom() {
		return random;
	}

	@Override
	public List<GridDirection> locationDirection(ILocation start, ILocation target) {
		int targetX = target.getX(), targetY = target.getY();
		int startX = start.getX(), startY = start.getY();
		List<GridDirection> dirs = new ArrayList<>();
		if (targetX > startX && targetY > startY) {
			if (Math.abs(targetX - startX) < Math.abs(targetY - startY)) {
				dirs.add(GridDirection.SOUTH);
				dirs.add(GridDirection.EAST);
			} else {
                dirs.add(GridDirection.EAST);
                dirs.add(GridDirection.SOUTH);
			}
		} else if (targetX > startX && targetY < startY) {
			if (Math.abs(targetX - startX) < Math.abs(targetY - startY)) {
                dirs.add(GridDirection.NORTH);
                dirs.add(GridDirection.EAST);
			} else {
                dirs.add(GridDirection.EAST);
                dirs.add(GridDirection.NORTH);
			}
		} else if (targetX < startX && targetY > startY) {
			if (Math.abs(targetX - startX) < Math.abs(targetY - startY)) {
                dirs.add(GridDirection.SOUTH);
                dirs.add(GridDirection.WEST);
			} else {
                dirs.add(GridDirection.WEST);
                dirs.add(GridDirection.SOUTH);
			}
		} else if (targetX < startX && targetY < startY) {
			if (Math.abs(targetX - startX) < Math.abs(targetY - startY)) {
                dirs.add(GridDirection.NORTH);
                dirs.add(GridDirection.WEST);
			} else {
                dirs.add(GridDirection.WEST);
                dirs.add(GridDirection.NORTH);
			}
		} else if (targetX > startX) {
            dirs.add(GridDirection.EAST);
		} else if (targetX < startX) {
            dirs.add(GridDirection.WEST);
		} else if (targetY > startY) {
            dirs.add(GridDirection.SOUTH);
		} else if (targetY < startY) {
            dirs.add(GridDirection.NORTH);
		}
		return dirs;
	}
}