import java.awt.*;
import java.applet.Applet;
import java.net.URL;

public class MapApplet extends Applet
{
	// The scrollbar objects
	Scrollbar vertical;
	Scrollbar horizontal;
	// Generate a new map when this is clicked
	Button newMap;
	// We treat the map canvas as a "window" into a larger
	// map. These are the coordinates of the upper left
	// corner of the "window", within the larger map
	int xoffset = 0;
	int yoffset = 0;
	// The component we'll draw the map in
	MapCanvas mapCanvas;
	// An array of images for use as tiles in the map
	Image tiles[];
	// Names for the different tile types
	static final int ocean = 0;
	static final int plain = 1;
	static final int desert = 2;
	static final int swamp = 3;
	static final int forest = 4;
	static final int hill = 5;
	static final int mountain = 6;
	// Total number of tile types. Be sure to update this if
	// you add or remove entries above
	static final int tilesTotal = 7;
	// The size of a tile in pixels. Tiles are always square
	static final int tileSize = 32;
	// The size of the map, in tiles
	int width = 32;
	int height = 32;	
	// Each entry in this array is a tile id number indicating
	// which tile to draw at that location
	int map[][];
	public void init()
	{
		// 1. Create the map.
		makeMap();
		// 2. Lay out the controls.
		if (!makeComponents()) {
			return;
		}
		// 3. Get the images.
		if (!loadImages()) {
			return;
		}
	}
	boolean loadImages()
	{
		// Find out where the images are
		String baseurlString = getParameter("baseurl");
		if (baseurlString == null) {
			System.out.println(
				"<param name=\"baseurl\"> tag missing!");
			return false;
		}
		// Now go get them
		tiles = new Image[tilesTotal];
		int i;

		// getImage returns right away; the image then loads
		// progressively. We want to make sure all of the images
		// load completely before going on, so we use a handy
		// class known as MediaTracker.

		MediaTracker mt = new MediaTracker(this);
		for (i = 0; (i < 7); i++) {
			URL url;
			String urlString = "not yet known";
			try {
				urlString = baseurlString + 
					"/tile" + Integer.toString(i) + ".gif";
				url = new URL(urlString);
			} catch (java.net.MalformedURLException e) {
				System.out.println(urlString + 
					" tile missing!");
				return false;
			}
			// Fetch the image tile and add it to the
			// media tracker.
			tiles[i] = getImage(url);
			mt.addImage(tiles[i], 0);
		}
		// Ask the media tracker to wait until all of the
		// images have loaded completely.
		try {
			mt.waitForAll();
		} catch (InterruptedException e) {
			// This shouldn't happen
			System.out.println(e);
		}
		return true;
	}
	boolean makeComponents()
	{
		// A typical GridBagLayout as in many other applets.

		GridBagLayout layout = new GridBagLayout();
		GridBagConstraints c = new GridBagConstraints();
		setLayout(layout);
		c.fill = GridBagConstraints.BOTH;
		c.weightx = 1.0;
		c.weighty = 1.0;
		c.gridx = 0;
		c.gridy = 0;
		mapCanvas = new MapCanvas(this);
		layout.setConstraints(mapCanvas, c);
		add(mapCanvas);
		c.fill = GridBagConstraints.VERTICAL;
		c.weightx = 0.0;
		c.weighty = 1.0;
		c.gridx = 1;
		c.gridy = 0;
		vertical = new Scrollbar(Scrollbar.VERTICAL);
		layout.setConstraints(vertical, c);
		add(vertical);
		c.fill = GridBagConstraints.HORIZONTAL;
		c.weightx = 1.0;
		c.weighty = 0.0;
		c.gridx = 0;
		c.gridy = 1;
		horizontal = new Scrollbar(Scrollbar.HORIZONTAL);
		layout.setConstraints(horizontal, c);
		add(horizontal);
		c.fill = GridBagConstraints.HORIZONTAL;
		c.weightx = 1.0;
		c.weighty = 0.0;
		c.gridwidth = 2;
		c.gridx = 0;
		c.gridy = 2;
		newMap = new Button("New Map");
		layout.setConstraints(newMap, c);
		add(newMap);

		// Do the layout now. This makes sure the
		// real controls (such as Windows scrollbars) have
		// been created. No, this shouldn't be necessary
		// before proceeding, but inconsistencies between
		// java implementations make it a Good Idea to do this.
		layout();

		// Tell the scrollbars what their parameters will be.
		setScrollbars();

		return true;
	}
	void setScrollbars()
	{
		Rectangle r = mapCanvas.bounds();
		// Find the size in pixels for the entire map
		int wpixels = width * tileSize;
		int hpixels = height * tileSize;
		// Update the scrollbar settings.
		horizontal.setValues(xoffset, r.width, 0, 
			wpixels - 1 - r.width);
		vertical.setValues(yoffset, r.height, 0, 
			hpixels - 1 - r.height);
		// Look for Netscape for Windows, which has
		// a bug: setLineIncrement and setPageIncrement
		// are backwards. The system properties are useful
		// when you absolutely must find out something
		// about the real operating system and browser
		// being used.
		String vendor = System.getProperty("java.vendor");
		// This is the version of java, not of Netscape...
		String version = System.getProperty("java.version");
		String osname = System.getProperty("os.name");
		boolean reversed = false;
		if (osname.startsWith("Windows")) {
			if (vendor.startsWith("Netscape")) {
				double d = 0.0;
				try {
					d = Double.valueOf(version).
						doubleValue();
				} catch (Exception e) {
				}
				if (d < 1.1) {
					// The bug is present.
					reversed = true;
				} else {
					// Let's hope Netscape fixes this
					// bug when they go to Java 1.1!
				}
			}
		}
		if (reversed) {
			// Cope with a Netscape for Windows bug.
			vertical.setPageIncrement(tileSize);
			horizontal.setPageIncrement(tileSize);
			vertical.setLineIncrement(r.height);
			horizontal.setLineIncrement(r.width);
		} else {
			vertical.setLineIncrement(tileSize);
			horizontal.setLineIncrement(tileSize);
			vertical.setPageIncrement(r.height);
			horizontal.setPageIncrement(r.width);
		}
	}
	public boolean action(Event evt, Object obj)
	{
		// Respond to the newMap button.
		if (evt.target == newMap) {
			makeMap();
			mapCanvas.repaint();
			return true;
		}
		return false;
	}
	public boolean keyDown(Event evt, int key)
	{
		// Here we are concerned with scrolling
		// the display when the user presses
		// arrow keys.
		//
		Rectangle r = mapCanvas.bounds();
		// Find the size in pixels for the entire map
		int wpixels = width * tileSize;
		int hpixels = height * tileSize;
		if (key == Event.UP) {
			// Subtract from the y axis offset,
			// then make sure we're still within bounds.
			yoffset -= tileSize;
			if (yoffset < 0) {
				yoffset = 0;
			}
			vertical.setValue(yoffset);
			mapCanvas.repaint();
			return true;
		} else if (key == Event.DOWN) {
			// Add to the y axis offset,
			// then make sure we're still within bounds.
			yoffset += tileSize;
			if (yoffset >= hpixels - r.height) {
				yoffset = hpixels - r.height;
			}
			vertical.setValue(yoffset);
			mapCanvas.repaint();
			return true;
		} else if (key == Event.LEFT) {
			// Subtract from the x axis offset,
			// then make sure we're still within bounds.
			xoffset -= tileSize;
			if (xoffset < 0) {
				xoffset = 0;
			}
			horizontal.setValue(xoffset);
			mapCanvas.repaint();
			return true;
		} else if (key == Event.RIGHT) {
			// Add to the x axis offset,
			// then make sure we're still within bounds.
			xoffset += tileSize;
			if (xoffset >= wpixels - r.width) {
				xoffset = wpixels - r.width;
			}
			horizontal.setValue(xoffset);			
			mapCanvas.repaint();
			return true;
		}
		return false;
	}
	// handleEvent is replaced here in order to catch
	// events coming from the scrollbars. 
	public boolean handleEvent(Event event) {
		int newXOffset;
		int newYOffset;
		if (event.target == horizontal) {
			switch (event.id) {
				case Event.SCROLL_ABSOLUTE:
				case Event.SCROLL_LINE_DOWN:
				case Event.SCROLL_LINE_UP:
				case Event.SCROLL_PAGE_DOWN:
				case Event.SCROLL_PAGE_UP:
				// Netscape for Windows likes to send
				// an awful lot of SCROLL_ABSOLUTE events
				// for the same position, so just check
				// to make sure there's a real change.
				newXOffset = horizontal.getValue();
				// Round it off to the nearest tile.
				// This helps performance too by ignoring
				// a lot of small steps. 
				newXOffset -= (newXOffset % tileSize);
				// Now see if there's a real change
				if (newXOffset != xoffset) {
					xoffset = newXOffset;
					mapCanvas.repaint();
				}
				break;
			}
		} else if (event.target == vertical) {
			switch (event.id) {
				case Event.SCROLL_ABSOLUTE:
				case Event.SCROLL_LINE_DOWN:
				case Event.SCROLL_LINE_UP:
				case Event.SCROLL_PAGE_DOWN:
				case Event.SCROLL_PAGE_UP:
				// Netscape for Windows likes to send
				// an awful lot of SCROLL_ABSOLUTE events
				// for the same position, so just check
				// to make sure there's a real change.
				newYOffset = vertical.getValue();
				// Round it off to the nearest tile.
				// This helps performance too by ignoring
				// a lot of steps. 
				newYOffset -= (newYOffset % tileSize);
				// Now see if there's a real change
				if (newYOffset != yoffset) {
					yoffset = newYOffset;
					mapCanvas.repaint();
				}
				break;
			}
		} 
		return super.handleEvent(event);
	}    
	void paintMap(Graphics g)
	{
		// Loop through all of the locations in 
		// the map. Figure out where they are in
		// pixel terms, subtract the current
		// upper left corner, and paint the
		// appropriate image there.
		int x, y;
		// How much space is available in the canvas?
		int yspace = bounds().height;
		int xspace = bounds().width;
		// How many rows are visible?
		int visibleRows = yspace / tileSize + 1;
		// How many columns are visible?
		int visibleColumns = xspace / tileSize + 1;
		// What row and column are at the corner?
		int cornerRow = yoffset / tileSize;
		int cornerColumn = xoffset / tileSize;
		for (y = 0; (y < height); y++) {
			// If this row is not visible, then skip it.
			if ((y < (cornerRow - 1)) || 
				(y > cornerRow + visibleRows))
			{
				continue;
			}
			for (x = 0; (x < width); x++) {
				// If this column is not visible, 
				// then skip it.
				if ((x < (cornerColumn - 1)) || 
					(x > cornerColumn + visibleColumns))
				{
					continue;
				}
				g.drawImage(
					tiles[map[y][x]],
					x * tileSize - xoffset, 
					y * tileSize - yoffset, 
					mapCanvas);
			}
		}
	}
	void makeMap()
	{
		// Our world is made of volcanic islands which
		// start out as a single very tall volcano, and
		// are then eroded for a while. We then decide
		// what terrain goes where based on the altitude
		// of each square. 
		// The number of squares that are filled so far
		int full = 0;
		map = new int[height][width];
		int altitude[][] = new int[height][width];
		int newAltitude[][] = new int[height][width];
		int x, y;
		// In the beginning, geek made the ocean.
		for (y = 0; (y < height); y++) {
			for (x = 0; (x < width); x++) {
				map[y][x] = ocean;
				altitude[y][x] = 0;
			}
		}
		// Then geek made some big volcanoes!
		int seedsTotal = 4;
		int seed;
		for (seed = 0; (seed < seedsTotal); seed++) {
			// Pick reasonable locations.
			x = randomBetween(2, width - 2);
			y = randomBetween(2, height - 2);
			// Plug them into the map.
			map[y][x] = mountain;
			// One hundred somethingummies tall
			altitude[y][x] = 100;
			// This square is now full
			full++;
		}
	
	 	// Then geek simulated many millenia of erosion,
		// spreading the wealth out among adjacent squares.
		// Sort of: we don't really lower the original squares.
		//
		// Don't not let the altitude drop to zero;
		// carefully increment full as each hex is
		// filled. At 80% full or so, or after 100 passes,
		// call it quits.

		int acceptable = (width * height) * 8 / 10;
		int passes = 0;

		while ((full < acceptable) && (passes < 100)) {
			for (y = 0; (y < height); y++) {
				for (x = 0; (x < width); x++) {
					// Start determining the new
					// altitude for this cell.
					newAltitude[y][x] = altitude[y][x];
					if (altitude[y][x] == 0) {
						// Look at the neighbors
						// and determine how high
						// this square ought to be.
						if (fillNewCell(
							altitude,
							newAltitude,
							y, x))
						{
							full++;
						}
					}
				}
			}
			// Set the new altitudes for the next pass.
			for (y = 0; (y < height); y++) {
				for (x = 0; (x < width); x++) {
					altitude[y][x] = newAltitude[y][x];
				}
			}
			passes++;
		}
	}
	// Figure out what to do with one cell.
	// Returns true if the cell was given
	// an altitude above zero on this pass.
	boolean fillNewCell(
		int[][] altitude, 
		int[][] newAltitude,
		int y, int x)
	{
		int totalAltitude = 0;
		int fullNeighbors = 0;
		int subx, suby;
		totalAltitude = 0;
		for (suby = (-1); (suby <= 1); suby++) {
			for (subx = (-1); (subx <= 1); subx++) {
				int mapy, mapx;
				mapy = y + suby;
				mapx = x + subx;
				if (legal(mapy, mapx)) {
					int a = altitude[mapy][mapx];
					if (a > 0) {
						totalAltitude += a;
						fullNeighbors++;
					}
				}
			}
		}
		if (totalAltitude > 0) {
			newAltitude[y][x] = 
				(totalAltitude / fullNeighbors) -
					randomBetween(0, 20);
			// Determine the terrain. 
			// This is done based on the
			// altitude, with a small random bias.
			int placement = newAltitude[y][x]
				+ randomBetween(-10, 10);
			if (placement > 90) {
				map[y][x] = mountain;
			} else if (placement > 70) {
				map[y][x] = hill;
			} else if (placement > 50) {
				map[y][x] = desert;
			} else if (placement > 30) {
				map[y][x] = forest;
			} else if (placement > 10) {
				map[y][x] = plain;
			} else if (placement > 0) {
				map[y][x] = swamp; 
			} else {
				map[y][x] = ocean;
			}
			return true;
		} else {
			return false;
		}
	}
	// A random integer between these two (inclusive)
	int randomBetween(int low, int high)
	{
		int value = (int) (Math.random() * (high - low + 1) + low);
		// Just in case "any value between 0.00 and 1.00" actually
		// turns out to be 1.00, which isn't very likely...
		if (value == high + 1) {
			value = high;
		}
		return value;
	}
	// Is this location within the map?
	boolean legal(int y, int x)
	{
		if ((x < 0) || (x >= width)) {
			return false;
		}
		if ((y < 0) || (y >= height)) {
			return false;
		}
		return true;
	}
}

// This class is a simple "slave" canvas which asks
// its owner to paint it.

class MapCanvas extends Canvas
{
	MapApplet owner;
	MapCanvas(MapApplet ownerArg)
	{
		owner = ownerArg;
	}	
	public void paint(Graphics g)
	{
		owner.paintMap(g);
	}
}

